diff --git a/src/it/java/teammates/it/sqllogic/core/FeedbackSessionLogsLogicIT.java b/src/it/java/teammates/it/sqllogic/core/FeedbackSessionLogsLogicIT.java new file mode 100644 index 00000000000..1007cd50328 --- /dev/null +++ b/src/it/java/teammates/it/sqllogic/core/FeedbackSessionLogsLogicIT.java @@ -0,0 +1,131 @@ +package teammates.it.sqllogic.core; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.SqlDataBundle; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.HibernateUtil; +import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; +import teammates.sqllogic.core.FeedbackSessionLogsLogic; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link FeedbackSessionLogsLogic}. + */ +public class FeedbackSessionLogsLogicIT extends BaseTestCaseWithSqlDatabaseAccess { + + private FeedbackSessionLogsLogic fslLogic = FeedbackSessionLogsLogic.inst(); + + private SqlDataBundle typicalDataBundle; + + @Override + @BeforeClass + public void setupClass() { + super.setupClass(); + typicalDataBundle = getTypicalSqlDataBundle(); + } + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalDataBundle); + HibernateUtil.flushSession(); + HibernateUtil.clearSession(); + } + + @Test + public void test_createFeedbackSessionLog_success() { + Course course = typicalDataBundle.courses.get("course1"); + FeedbackSession fs = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + Student student = typicalDataBundle.students.get("student1InCourse1"); + Instant timestamp = Instant.now(); + FeedbackSessionLog newLog1 = new FeedbackSessionLog(student, fs, FeedbackSessionLogType.ACCESS, timestamp); + FeedbackSessionLog newLog2 = new FeedbackSessionLog(student, fs, FeedbackSessionLogType.SUBMISSION, timestamp); + FeedbackSessionLog newLog3 = new FeedbackSessionLog(student, fs, FeedbackSessionLogType.VIEW_RESULT, timestamp); + List expected = List.of(newLog1, newLog2, newLog3); + + fslLogic.createFeedbackSessionLogs(expected); + + List actual = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), student.getId(), + fs.getId(), timestamp, timestamp.plusSeconds(1)); + + assertEquals(expected, actual); + } + + @Test + public void test_getOrderedFeedbackSessionLogs_success() { + Instant startTime = Instant.parse("2012-01-01T12:00:00Z"); + Instant endTime = Instant.parse("2012-01-01T23:59:59Z"); + Course course = typicalDataBundle.courses.get("course1"); + Student student1 = typicalDataBundle.students.get("student1InCourse1"); + FeedbackSession fs1 = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + + FeedbackSessionLog student1Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session1Log1"); + FeedbackSessionLog student1Session2Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log1"); + FeedbackSessionLog student1Session2Log2 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log2"); + FeedbackSessionLog student2Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log1"); + FeedbackSessionLog student2Session1Log2 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log2"); + + ______TS("Return logs belonging to a course in time range"); + List expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2, + student2Session1Log1, + student2Session1Log2); + + List actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), null, null, + startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in a course in time range"); + expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), null, startTime, + endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a feedback session in time range"); + expectedLogs = List.of( + student1Session1Log1, + student2Session1Log1, + student2Session1Log2); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), null, fs1.getId(), startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in a feedback session in time range"); + expectedLogs = List.of(student1Session1Log1); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), fs1.getId(), + startTime, + endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("No logs in time range, return empty list"); + expectedLogs = new ArrayList<>(); + + actualLogs = fslLogic.getOrderedFeedbackSessionLogs(course.getId(), null, null, endTime.plusSeconds(3600), + endTime.plusSeconds(7200)); + + assertEquals(expectedLogs, actualLogs); + } + +} diff --git a/src/it/java/teammates/it/storage/sqlapi/FeedbackSessionLogsDbIT.java b/src/it/java/teammates/it/storage/sqlapi/FeedbackSessionLogsDbIT.java new file mode 100644 index 00000000000..30b5276893b --- /dev/null +++ b/src/it/java/teammates/it/storage/sqlapi/FeedbackSessionLogsDbIT.java @@ -0,0 +1,128 @@ +package teammates.it.storage.sqlapi; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.SqlDataBundle; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.HibernateUtil; +import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; +import teammates.storage.sqlapi.FeedbackSessionLogsDb; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * SUT: {@link FeedbackSessionLogsDb}. + */ +public class FeedbackSessionLogsDbIT extends BaseTestCaseWithSqlDatabaseAccess { + + private final FeedbackSessionLogsDb fslDb = FeedbackSessionLogsDb.inst(); + + private SqlDataBundle typicalDataBundle; + + @Override + @BeforeClass + public void setupClass() { + super.setupClass(); + typicalDataBundle = getTypicalSqlDataBundle(); + } + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalDataBundle); + HibernateUtil.flushSession(); + } + + @Test + public void test_createFeedbackSessionLog_success() { + Course course = typicalDataBundle.courses.get("course1"); + FeedbackSession feedbackSession = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + Student student = typicalDataBundle.students.get("student1InCourse1"); + + Instant logTimestamp = Instant.parse("2011-01-01T00:00:00Z"); + FeedbackSessionLog expected = new FeedbackSessionLog(student, feedbackSession, FeedbackSessionLogType.ACCESS, + logTimestamp); + + fslDb.createFeedbackSessionLog(expected); + + List actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), student.getId(), + feedbackSession.getId(), logTimestamp, logTimestamp.plusSeconds(1)); + + assertEquals(actualLogs.size(), 1); + assertEquals(expected, actualLogs.get(0)); + } + + @Test + public void test_getOrderedFeedbackSessionLogs_success() { + Instant startTime = Instant.parse("2012-01-01T12:00:00Z"); + Instant endTime = Instant.parse("2012-01-01T23:59:59Z"); + Course course = typicalDataBundle.courses.get("course1"); + Student student1 = typicalDataBundle.students.get("student1InCourse1"); + FeedbackSession fs1 = typicalDataBundle.feedbackSessions.get("session1InCourse1"); + + FeedbackSessionLog student1Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session1Log1"); + FeedbackSessionLog student1Session2Log1 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log1"); + FeedbackSessionLog student1Session2Log2 = typicalDataBundle.feedbackSessionLogs.get("student1Session2Log2"); + FeedbackSessionLog student2Session1Log1 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log1"); + FeedbackSessionLog student2Session1Log2 = typicalDataBundle.feedbackSessionLogs.get("student2Session1Log2"); + + ______TS("Return logs belonging to a course in time range"); + List expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2, + student2Session1Log1, + student2Session1Log2 + ); + + List actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), null, null, + startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in time range"); + expectedLogs = List.of( + student1Session1Log1, + student1Session2Log1, + student1Session2Log2); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), null, startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a feedback session in time range"); + expectedLogs = List.of( + student1Session1Log1, + student2Session1Log1, + student2Session1Log2); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), null, fs1.getId(), startTime, endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("Return logs belonging to a student in a feedback session in time range"); + expectedLogs = List.of(student1Session1Log1); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), fs1.getId(), startTime, + endTime); + + assertEquals(expectedLogs, actualLogs); + + ______TS("No logs in time range, return empty list"); + expectedLogs = new ArrayList<>(); + + actualLogs = fslDb.getOrderedFeedbackSessionLogs(course.getId(), null, null, endTime.plusSeconds(3600), + endTime.plusSeconds(7200)); + + assertEquals(expectedLogs, actualLogs); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/CreateFeedbackSessionLogActionIT.java b/src/it/java/teammates/it/ui/webapi/CreateFeedbackSessionLogActionIT.java new file mode 100644 index 00000000000..c15c45752cc --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/CreateFeedbackSessionLogActionIT.java @@ -0,0 +1,160 @@ +package teammates.it.ui.webapi; + +import java.util.UUID; + +import org.testng.annotations.Test; + +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.Student; +import teammates.ui.output.MessageOutput; +import teammates.ui.webapi.CreateFeedbackSessionLogAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link CreateFeedbackSessionLogAction}. + */ +public class CreateFeedbackSessionLogActionIT extends BaseActionIT { + + @Override + protected String getActionUri() { + return Const.ResourceURIs.SESSION_LOGS; + } + + @Override + protected String getRequestMethod() { + return POST; + } + + @Test + @Override + protected void testExecute() throws Exception { + Course course1 = typicalBundle.courses.get("course1"); + String courseId1 = course1.getId(); + FeedbackSession fs1 = typicalBundle.feedbackSessions.get("session1InCourse1"); + FeedbackSession fs2 = typicalBundle.feedbackSessions.get("session2InTypicalCourse"); + Student student1 = typicalBundle.students.get("student1InCourse1"); + Student student2 = typicalBundle.students.get("student2InCourse1"); + Student student3 = typicalBundle.students.get("student1InCourse3"); + + ______TS("Failure case: not enough parameters"); + verifyHttpParameterFailure(Const.ParamsNames.COURSE_ID, courseId1); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName() + ); + verifyHttpParameterFailure( + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail() + ); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.ACCESS.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail() + ); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_ID, fs2.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_SQL_ID, student2.getId().toString() + ); + + ______TS("Failure case: invalid log type"); + String[] paramsInvalid = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, "invalid log type", + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + verifyHttpParameterFailure(paramsInvalid); + + ______TS("Success case: typical access"); + String[] paramsSuccessfulAccess = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.ACCESS.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + JsonResult response = getJsonResult(getAction(paramsSuccessfulAccess)); + MessageOutput output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: typical submission"); + String[] paramsSuccessfulSubmission = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs2.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student2.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs2.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student2.getId().toString(), + }; + response = getJsonResult(getAction(paramsSuccessfulSubmission)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: should create even for invalid parameters"); + String[] paramsNonExistentCourseId = { + Const.ParamsNames.COURSE_ID, "non-existent-course-id", + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + response = getJsonResult(getAction(paramsNonExistentCourseId)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: should create even for invalid parameters"); + String[] paramsNonExistentFsName = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, "non-existent-feedback-session-name", + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, UUID.randomUUID().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + }; + response = getJsonResult(getAction(paramsNonExistentFsName)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + String[] paramsNonExistentStudentEmail = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, "non-existent-student@email.com", + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, UUID.randomUUID().toString(), + }; + response = getJsonResult(getAction(paramsNonExistentStudentEmail)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + + ______TS("Success case: should create even when student cannot access feedback session in course"); + String[] paramsWithoutAccess = { + Const.ParamsNames.COURSE_ID, courseId1, + Const.ParamsNames.FEEDBACK_SESSION_NAME, fs1.getName(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), + Const.ParamsNames.STUDENT_EMAIL, student3.getEmail(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.STUDENT_SQL_ID, student3.getId().toString(), + }; + response = getJsonResult(getAction(paramsWithoutAccess)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); + } + + @Test + @Override + protected void testAccessControl() throws Exception { + verifyAnyUserCanAccess(); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java b/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java index 2ee319637ae..238e9175465 100644 --- a/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/GetFeedbackSessionLogsActionIT.java @@ -49,27 +49,12 @@ protected void testExecute() { Course course = typicalBundle.courses.get("course1"); String courseId = course.getId(); FeedbackSession fsa1 = typicalBundle.feedbackSessions.get("session1InCourse1"); - FeedbackSession fsa2 = typicalBundle.feedbackSessions.get("session2InTypicalCourse"); - String fsa1Name = fsa1.getName(); - String fsa2Name = fsa2.getName(); Student student1 = typicalBundle.students.get("student1InCourse1"); Student student2 = typicalBundle.students.get("student2InCourse1"); String student1Email = student1.getEmail(); String student2Email = student2.getEmail(); - long endTime = Instant.now().toEpochMilli(); + long endTime = Instant.parse("2012-01-02T12:00:00Z").toEpochMilli(); long startTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() - 1) * 24 * 60 * 60 * 1000; - long invalidStartTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() + 1) * 24 * 60 * 60 * 1000; - - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa1Name, - FeedbackSessionLogType.ACCESS.getLabel(), startTime); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, - FeedbackSessionLogType.ACCESS.getLabel(), startTime + 1000); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, - FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 2000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, - FeedbackSessionLogType.ACCESS.getLabel(), startTime + 3000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, - FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 4000); ______TS("Failure case: not enough parameters"); verifyHttpParameterFailure( @@ -87,16 +72,16 @@ protected void testExecute() { ______TS("Failure case: invalid course id"); String[] paramsInvalid1 = { Const.ParamsNames.COURSE_ID, "fake-course-id", - Const.ParamsNames.STUDENT_EMAIL, student1Email, + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), }; verifyEntityNotFound(paramsInvalid1); - ______TS("Failure case: invalid student email"); + ______TS("Failure case: invalid student id"); String[] paramsInvalid2 = { Const.ParamsNames.COURSE_ID, courseId, - Const.ParamsNames.STUDENT_EMAIL, "fake-student-email@gmail.com", + Const.ParamsNames.STUDENT_SQL_ID, "00000000-0000-0000-0000-000000000000", Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), }; @@ -117,13 +102,6 @@ protected void testExecute() { }; verifyHttpParameterFailure(paramsInvalid4); - ______TS("Failure case: start time is before earliest search time"); - verifyHttpParameterFailure( - Const.ParamsNames.COURSE_ID, courseId, - Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(invalidStartTime), - Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime) - ); - ______TS("Success case: should group by feedback session"); String[] paramsSuccessful1 = { Const.ParamsNames.COURSE_ID, courseId, @@ -161,15 +139,63 @@ protected void testExecute() { assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1Email); assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); - ______TS("Success case: should accept optional email"); + ______TS("Success case: should accept optional student Id"); String[] paramsSuccessful2 = { Const.ParamsNames.COURSE_ID, courseId, - Const.ParamsNames.STUDENT_EMAIL, student1Email, + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), }; - getJsonResult(getAction(paramsSuccessful2)); - // No need to check output again here, it will be exactly the same as the previous case + actionOutput = getJsonResult(getAction(paramsSuccessful2)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 6); + assertEquals(fsLogs.get(2).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(3).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(4).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(5).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + fsLogEntries2 = fsLogs.get(1).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 1); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + + assertEquals(fsLogEntries2.size(), 2); + assertEquals(fsLogEntries2.get(0).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries2.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept optional feedback session"); + String[] paramsSuccessful3 = { + Const.ParamsNames.COURSE_ID, courseId, + Const.ParamsNames.FEEDBACK_SESSION_ID, fsa1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful3)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 6); + assertEquals(fsLogs.get(1).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(2).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(3).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(4).getFeedbackSessionLogEntries().size(), 0); + assertEquals(fsLogs.get(5).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 3); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1Email); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(1).getStudentData().getEmail(), student2Email); + assertEquals(fsLogEntries1.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(2).getStudentData().getEmail(), student2Email); + assertEquals(fsLogEntries1.get(2).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); // TODO: if we restrict the range from start to end time, it should be tested here as well } diff --git a/src/it/java/teammates/it/ui/webapi/UpdateFeedbackSessionLogsActionIT.java b/src/it/java/teammates/it/ui/webapi/UpdateFeedbackSessionLogsActionIT.java new file mode 100644 index 00000000000..b6f2a8b1f47 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/UpdateFeedbackSessionLogsActionIT.java @@ -0,0 +1,236 @@ +package teammates.it.ui.webapi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.FeedbackSessionLogEntry; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.common.util.HibernateUtil; +import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; +import teammates.storage.sqlsearch.SearchManagerFactory; +import teammates.ui.webapi.UpdateFeedbackSessionLogsAction; + +/** + * SUT: {@link UpdateFeedbackSessionLogsAction}. + */ +public class UpdateFeedbackSessionLogsActionIT extends BaseActionIT { + + static final long COLLECTION_TIME_PERIOD = Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes(); + static final long SPAM_FILTER = Const.STUDENT_ACTIVITY_LOGS_FILTER_WINDOW.toMillis(); + + Student student1InCourse1; + Student student2InCourse1; + Student student1InCourse3; + + Course course1; + Course course3; + + FeedbackSession session1InCourse1; + FeedbackSession session2InCourse1; + FeedbackSession session1InCourse3; + + Instant endTime; + Instant startTime; + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalBundle); + HibernateUtil.flushSession(); + SearchManagerFactory.getStudentSearchManager().resetCollections(); + + endTime = TimeHelper.getInstantNearestQuarterHourBefore(Instant.now()); + startTime = endTime.minus(COLLECTION_TIME_PERIOD, ChronoUnit.MINUTES); + + course1 = typicalBundle.courses.get("course1"); + course3 = typicalBundle.courses.get("course3"); + + student1InCourse1 = typicalBundle.students.get("student1InCourse1"); + student2InCourse1 = typicalBundle.students.get("student2InCourse1"); + student1InCourse3 = typicalBundle.students.get("student1InCourse3"); + + session1InCourse1 = typicalBundle.feedbackSessions.get("session1InCourse1"); + session2InCourse1 = typicalBundle.feedbackSessions.get("session2InTypicalCourse"); + session1InCourse3 = typicalBundle.feedbackSessions.get("ongoingSession1InCourse3"); + + mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, "").clear(); + } + + @Override + String getActionUri() { + return Const.CronJobURIs.AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING; + } + + @Override + String getRequestMethod() { + return GET; + } + + @Test + @Override + protected void testExecute() { + ______TS("No spam all logs added"); + // Different Types + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.SUBMISSION.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.VIEW_RESULT.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + + // Different feedback sessions + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(200).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session2InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(200).toEpochMilli()); + + // Different Student + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(300).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student2InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(300).toEpochMilli()); + + // Different course + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(400).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course3.getId(), student1InCourse3.getId(), + session1InCourse3.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(400).toEpochMilli()); + + // Gap is larger than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + getJsonResult(action); + + // method returns all logs regardless of params + List expected = mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, ""); + List actual = logic.getOrderedFeedbackSessionLogs(course1.getId(), null, null, startTime, + endTime); + List actualCourse3 = logic.getOrderedFeedbackSessionLogs(course3.getId(), null, null, + startTime, endTime); + actual.addAll(actualCourse3); + assertTrue(isEqual(expected, actual)); + } + + @Test + protected void testExecute_recentLogsWithSpam_someLogsCreated() { + // Gap is smaller than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER - 2).toEpochMilli()); + + // Filters multiple logs within one spam window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER - 1).toEpochMilli()); + + // Correctly adds new log after filtering + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + // Filters out spam in the new window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusMillis(SPAM_FILTER + 2).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + List expected = new ArrayList<>(); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli())); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli())); + + List actual = logic.getOrderedFeedbackSessionLogs(course1.getId(), null, null, startTime, + endTime); + assertTrue(isEqual(expected, actual)); + } + + @Test + protected void testExecute_badLogs_otherLogsCreated() { + UUID badUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), + session1InCourse1.getId(), FeedbackSessionLogType.ACCESS.getLabel(), + startTime.plusSeconds(300).toEpochMilli()); + + // bad student id + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), badUuid, session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + + // bad session id + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1InCourse1.getId(), badUuid, + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + List expected = new ArrayList<>(); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(100).toEpochMilli())); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1InCourse1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(300).toEpochMilli())); + + List actual = logic.getOrderedFeedbackSessionLogs(course1.getId(), null, null, startTime, + endTime); + assertTrue(isEqual(expected, actual)); + } + + @Test + @Override + protected void testAccessControl() throws Exception { + Course course = typicalBundle.courses.get("course1"); + verifyOnlyAdminCanAccess(course); + } + + private Boolean isEqual(List expected, List actual) { + + assertEquals(expected.size(), actual.size()); + + for (int i = 0; i < expected.size(); i++) { + FeedbackSessionLogEntry expectedEntry = expected.get(i); + FeedbackSessionLog actualLog = actual.get(i); + + assertEquals(expectedEntry.getStudentId(), actualLog.getStudent().getId()); + + assertEquals(expectedEntry.getFeedbackSessionId(), actualLog.getFeedbackSession().getId()); + + assertEquals(expectedEntry.getFeedbackSessionLogType(), actualLog.getFeedbackSessionLogType().getLabel()); + + assertEquals(expectedEntry.getTimestamp(), actualLog.getTimestamp().toEpochMilli()); + } + + return true; + } +} diff --git a/src/it/resources/data/typicalDataBundle.json b/src/it/resources/data/typicalDataBundle.json index 4c97cca8512..a04d0ef7193 100644 --- a/src/it/resources/data/typicalDataBundle.json +++ b/src/it/resources/data/typicalDataBundle.json @@ -1366,5 +1366,84 @@ "id": "00000000-0000-4000-8000-000000001101" } } + }, + "feedbackSessionLogs": { + "student1Session1Log1": { + "id": "00000000-0000-4000-8000-000000001301", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:00Z" + }, + "student1Session2Log1": { + "id": "00000000-0000-4000-8000-000000001302", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000702" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:01Z" + }, + "student1Session2Log2": { + "id": "00000000-0000-4000-8000-000000001303", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000702" + }, + "feedbackSessionLogType": "SUBMISSION", + "timestamp": "2012-01-01T12:00:02Z" + }, + "student2Session1Log1": { + "id": "00000000-0000-4000-8000-000000001304", + "student": { + "id" : "00000000-0000-4000-8000-000000000602" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:03Z" + }, + "student2Session1Log2": { + "id": "00000000-0000-4000-8000-000000001305", + "student": { + "id" : "00000000-0000-4000-8000-000000000602" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "SUBMISSION", + "timestamp": "2012-01-01T12:00:04Z" + }, + "student1InAnotherCourse": { + "id": "00000000-0000-4000-8000-000000001306", + "student": { + "id" : "00000000-0000-4000-8000-000000000601" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000707" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2012-01-01T12:00:05Z" + }, + "outOfRangeLog": { + "id": "00000000-0000-4000-8000-000000001307", + "student": { + "id" : "00000000-0000-4000-8000-000000000602" + }, + "feedbackSession": { + "id" : "00000000-0000-4000-8000-000000000701" + }, + "feedbackSessionLogType": "ACCESS", + "timestamp": "2010-01-01T00:00:00Z" + } } } diff --git a/src/main/appengine/cron.yaml b/src/main/appengine/cron.yaml index 07a94809826..f6426a84e1e 100644 --- a/src/main/appengine/cron.yaml +++ b/src/main/appengine/cron.yaml @@ -33,3 +33,7 @@ cron: schedule: 'every 5 minutes synchronized' timezone: 'Asia/Singapore' description: 'Compile severe logs and sends out email notifications.' +- url: '/auto/updateFeedbackSessionLogs' + schedule: 'every 15 minutes from 00:01 to 23:59' + timezone: 'Asia/Singapore' + description: 'Process feedback session activity logs from logging service and store in the database.' diff --git a/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java b/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java index 1502071050e..10608f179fb 100644 --- a/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java +++ b/src/main/java/teammates/common/datatransfer/FeedbackSessionLogEntry.java @@ -1,26 +1,57 @@ package teammates.common.datatransfer; +import java.util.UUID; + /** * Represents a log entry of a feedback session. */ -public class FeedbackSessionLogEntry { +public class FeedbackSessionLogEntry implements Comparable { + private final String courseId; + private final UUID studentId; private final String studentEmail; + private final UUID feedbackSessionId; private final String feedbackSessionName; private final String feedbackSessionLogType; private final long timestamp; - public FeedbackSessionLogEntry(String studentEmail, String feedbackSessionName, - String feedbackSessionLogType, long timestamp) { + public FeedbackSessionLogEntry(String courseId, String studentEmail, + String feedbackSessionName, String feedbackSessionLogType, long timestamp) { + this.courseId = courseId; + this.studentId = null; this.studentEmail = studentEmail; + this.feedbackSessionId = null; this.feedbackSessionName = feedbackSessionName; this.feedbackSessionLogType = feedbackSessionLogType; this.timestamp = timestamp; } + public FeedbackSessionLogEntry(String courseId, UUID studentId, UUID feedbackSessionId, + String feedbackSessionLogType, long timestamp) { + this.courseId = courseId; + this.studentId = studentId; + this.studentEmail = null; + this.feedbackSessionId = feedbackSessionId; + this.feedbackSessionName = null; + this.feedbackSessionLogType = feedbackSessionLogType; + this.timestamp = timestamp; + } + + public String getCourseId() { + return courseId; + } + + public UUID getStudentId() { + return studentId; + } + public String getStudentEmail() { return studentEmail; } + public UUID getFeedbackSessionId() { + return feedbackSessionId; + } + public String getFeedbackSessionName() { return feedbackSessionName; } @@ -32,4 +63,9 @@ public String getFeedbackSessionLogType() { public long getTimestamp() { return this.timestamp; } + + @Override + public int compareTo(FeedbackSessionLogEntry o) { + return Long.compare(this.getTimestamp(), o.getTimestamp()); + } } diff --git a/src/main/java/teammates/common/datatransfer/SqlDataBundle.java b/src/main/java/teammates/common/datatransfer/SqlDataBundle.java index d3a027b2775..b411b4ea094 100644 --- a/src/main/java/teammates/common/datatransfer/SqlDataBundle.java +++ b/src/main/java/teammates/common/datatransfer/SqlDataBundle.java @@ -11,6 +11,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.ReadNotification; @@ -37,6 +38,7 @@ public class SqlDataBundle { public Map feedbackQuestions = new LinkedHashMap<>(); public Map feedbackResponses = new LinkedHashMap<>(); public Map feedbackResponseComments = new LinkedHashMap<>(); + public Map feedbackSessionLogs = new LinkedHashMap<>(); public Map notifications = new LinkedHashMap<>(); public Map readNotifications = new LinkedHashMap<>(); } diff --git a/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java b/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java index 0563cada134..f3e765eb03e 100644 --- a/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java +++ b/src/main/java/teammates/common/datatransfer/logs/FeedbackSessionAuditLogDetails.java @@ -10,8 +10,12 @@ public class FeedbackSessionAuditLogDetails extends LogDetails { @Nullable private String courseId; @Nullable + private String feedbackSessionId; + @Nullable private String feedbackSessionName; @Nullable + private String studentId; + @Nullable private String studentEmail; private String accessType; @@ -51,11 +55,29 @@ public void setAccessType(String accessType) { this.accessType = accessType; } + public String getFeedbackSessionId() { + return feedbackSessionId; + } + + public void setFeedbackSessionId(String feedbackSessionId) { + this.feedbackSessionId = feedbackSessionId; + } + + public String getStudentId() { + return studentId; + } + + public void setStudentId(String studentId) { + this.studentId = studentId; + } + @Override public void hideSensitiveInformation() { courseId = null; feedbackSessionName = null; studentEmail = null; + studentId = null; + feedbackSessionId = null; } } diff --git a/src/main/java/teammates/common/util/Const.java b/src/main/java/teammates/common/util/Const.java index ee741c14c1f..dc9659f867b 100644 --- a/src/main/java/teammates/common/util/Const.java +++ b/src/main/java/teammates/common/util/Const.java @@ -45,6 +45,9 @@ public final class Const { public static final String MISSING_RESPONSE_TEXT = "No Response"; + public static final Duration STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL = Duration.ofMinutes(15); + public static final Duration STUDENT_ACTIVITY_LOGS_FILTER_WINDOW = Duration.ofSeconds(2); + public static final String ACCOUNT_REQUEST_NOT_FOUND = "Account request with id = %s not found"; // These constants are used as variable values to mean that the variable is in a 'special' state. @@ -125,6 +128,8 @@ public static class ParamsNames { public static final String IS_CREATING_ACCOUNT = "iscreatingaccount"; public static final String IS_INSTRUCTOR = "isinstructor"; + public static final String FEEDBACK_SESSION_ID = "fsid"; + public static final String ACCOUNT_REQUEST_ID = "id"; public static final String ACCOUNT_REQUEST_STATUS = "status"; @@ -149,6 +154,7 @@ public static class ParamsNames { public static final String PREVIEWAS = "previewas"; + public static final String STUDENT_SQL_ID = "studentid"; public static final String STUDENT_ID = "googleid"; public static final String INVITER_ID = "invitergoogleid"; @@ -410,6 +416,8 @@ public static class CronJobURIs { URI_PREFIX + "/feedbackSessionPublishedReminders"; public static final String AUTOMATED_USAGE_STATISTICS_COLLECTION = URI_PREFIX + "/calculateUsageStatistics"; + public static final String AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING = + URI_PREFIX + "/updateFeedbackSessionLogs"; } /** diff --git a/src/main/java/teammates/common/util/HibernateUtil.java b/src/main/java/teammates/common/util/HibernateUtil.java index 8edcca6c807..0a1e4311e16 100644 --- a/src/main/java/teammates/common/util/HibernateUtil.java +++ b/src/main/java/teammates/common/util/HibernateUtil.java @@ -19,6 +19,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.ReadNotification; @@ -91,7 +92,8 @@ public final class HibernateUtil { FeedbackRankRecipientsResponse.class, FeedbackRubricResponse.class, FeedbackTextResponse.class, - FeedbackResponseComment.class); + FeedbackResponseComment.class, + FeedbackSessionLog.class); private HibernateUtil() { // Utility class @@ -282,4 +284,14 @@ public static void executeDelete(CriteriaDelete cd) { HibernateUtil.getCurrentSession().createMutationQuery(cd).executeUpdate(); } + /** + * Return a reference to the persistent instance with the given class and + * identifier,making the assumption that the instance is still persistent in the + * database. + * @see Session#getReference(Class, Object) + */ + public static T getReference(Class entityType, Object id) { + return HibernateUtil.getCurrentSession().getReference(entityType, id); + } + } diff --git a/src/main/java/teammates/common/util/TimeHelper.java b/src/main/java/teammates/common/util/TimeHelper.java index 8122ff6f39a..de7d58f796a 100644 --- a/src/main/java/teammates/common/util/TimeHelper.java +++ b/src/main/java/teammates/common/util/TimeHelper.java @@ -29,6 +29,18 @@ public static Instant getInstantNearestHourBefore(Instant instant) { return parseInstant(nearestHourString); } + /** + * Returns an Instant that represents the nearest quarter hour before the given object. + * + *

The time zone used is assumed to be the default timezone, namely UTC. + */ + public static Instant getInstantNearestQuarterHourBefore(Instant instant) { + ZonedDateTime zdt = instant.atZone(ZoneId.of(Const.DEFAULT_TIME_ZONE)); + int minutesPastQuarter = zdt.getMinute() % 15; + ZonedDateTime nearestQuarterZdt = zdt.minusMinutes(minutesPastQuarter).withSecond(0).withNano(0); + return nearestQuarterZdt.toInstant(); + } + /** * Returns an Instant that is offset by a number of days from now. * diff --git a/src/main/java/teammates/logic/api/LogsProcessor.java b/src/main/java/teammates/logic/api/LogsProcessor.java index eac5f73f755..57d889be488 100644 --- a/src/main/java/teammates/logic/api/LogsProcessor.java +++ b/src/main/java/teammates/logic/api/LogsProcessor.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.List; +import java.util.UUID; import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.QueryLogsResults; @@ -51,12 +52,19 @@ public void createFeedbackSessionLog(String courseId, String email, String fsNam } /** - * Gets the feedback session logs as filtered by the given parameters. + * Creates a feedback session log. + */ + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { + service.createFeedbackSessionLog(courseId, studentId, fsId, fslType); + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by ascending timestamp. * @param email Can be null */ - public List getFeedbackSessionLogs(String courseId, String email, + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { - return service.getFeedbackSessionLogs(courseId, email, startTime, endTime, fsName); + return service.getOrderedFeedbackSessionLogs(courseId, email, startTime, endTime, fsName); } /** diff --git a/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java b/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java index 3c2b7000ce7..be758f8809c 100644 --- a/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java +++ b/src/main/java/teammates/logic/external/GoogleCloudLoggingService.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import com.google.api.gax.paging.Page; @@ -115,7 +116,14 @@ public void createFeedbackSessionLog(String courseId, String email, String fsNam } @Override - public List getFeedbackSessionLogs(String courseId, String email, + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { + // This method is not necessary for production usage because a feedback session log + // is already separately created through the standardized logging infrastructure. + // However, this method is not removed as it is necessary to assist in local testing. + } + + @Override + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { List filters = new ArrayList<>(); if (courseId != null) { @@ -131,6 +139,7 @@ public List getFeedbackSessionLogs(String courseId, Str .withLogEvent(LogEvent.FEEDBACK_SESSION_AUDIT.name()) .withSeverityLevel(LogSeverity.INFO) .withExtraFilters(String.join("\n", filters)) + .withOrder(ASCENDING_ORDER) .build(); LogSearchParams logSearchParams = LogSearchParams.from(queryLogsParams) .addLogName(STDOUT_LOG_NAME) @@ -153,8 +162,16 @@ public List getFeedbackSessionLogs(String courseId, Str continue; } - FeedbackSessionLogEntry fslEntry = new FeedbackSessionLogEntry(details.getStudentEmail(), - details.getFeedbackSessionName(), details.getAccessType(), timestamp); + UUID studentId = details.getStudentId() != null ? UUID.fromString(details.getStudentId()) : null; + UUID fsId = details.getFeedbackSessionId() != null ? UUID.fromString(details.getFeedbackSessionId()) : null; + FeedbackSessionLogEntry fslEntry; + if (fsId != null && studentId != null) { + fslEntry = new FeedbackSessionLogEntry(details.getCourseId(), studentId, fsId, details.getAccessType(), + timestamp); + } else { + fslEntry = new FeedbackSessionLogEntry(details.getCourseId(), details.getStudentEmail(), + details.getFeedbackSessionName(), details.getAccessType(), timestamp); + } fsLogEntries.add(fslEntry); } diff --git a/src/main/java/teammates/logic/external/LocalLoggingService.java b/src/main/java/teammates/logic/external/LocalLoggingService.java index 03c90f52b4a..9750206fa37 100644 --- a/src/main/java/teammates/logic/external/LocalLoggingService.java +++ b/src/main/java/teammates/logic/external/LocalLoggingService.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -203,13 +204,20 @@ private boolean isRequestFilterSatisfied(LogDetails details, String actionClassF @Override public void createFeedbackSessionLog(String courseId, String email, String fsName, String fslType) { - FeedbackSessionLogEntry logEntry = new FeedbackSessionLogEntry(email, fsName, + FeedbackSessionLogEntry logEntry = new FeedbackSessionLogEntry(courseId, email, + fsName, fslType, Instant.now().toEpochMilli()); + FEEDBACK_SESSION_LOG_ENTRIES.computeIfAbsent(courseId, k -> new ArrayList<>()).add(logEntry); + } + + @Override + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { + FeedbackSessionLogEntry logEntry = new FeedbackSessionLogEntry(courseId, studentId, fsId, fslType, Instant.now().toEpochMilli()); FEEDBACK_SESSION_LOG_ENTRIES.computeIfAbsent(courseId, k -> new ArrayList<>()).add(logEntry); } @Override - public List getFeedbackSessionLogs(String courseId, String email, + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { return FEEDBACK_SESSION_LOG_ENTRIES .getOrDefault(courseId, new ArrayList<>()) @@ -218,6 +226,7 @@ public List getFeedbackSessionLogs(String courseId, Str .filter(log -> fsName == null || log.getFeedbackSessionName().equals(fsName)) .filter(log -> log.getTimestamp() >= startTime) .filter(log -> log.getTimestamp() <= endTime) + .sorted() .collect(Collectors.toList()); } diff --git a/src/main/java/teammates/logic/external/LogService.java b/src/main/java/teammates/logic/external/LogService.java index 5a85c59fafb..08f3653d2d3 100644 --- a/src/main/java/teammates/logic/external/LogService.java +++ b/src/main/java/teammates/logic/external/LogService.java @@ -1,6 +1,7 @@ package teammates.logic.external; import java.util.List; +import java.util.UUID; import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.QueryLogsResults; @@ -22,8 +23,13 @@ public interface LogService { void createFeedbackSessionLog(String courseId, String email, String fsName, String fslType); /** - * Gets the feedback session logs as filtered by the given parameters. + * Creates a feedback session log for migrated courses. */ - List getFeedbackSessionLogs(String courseId, String email, + void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType); + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by ascending timestamp. + */ + List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName); } diff --git a/src/main/java/teammates/sqllogic/api/Logic.java b/src/main/java/teammates/sqllogic/api/Logic.java index a70ea6b255d..86f749b5610 100644 --- a/src/main/java/teammates/sqllogic/api/Logic.java +++ b/src/main/java/teammates/sqllogic/api/Logic.java @@ -30,6 +30,7 @@ import teammates.sqllogic.core.FeedbackQuestionsLogic; import teammates.sqllogic.core.FeedbackResponseCommentsLogic; import teammates.sqllogic.core.FeedbackResponsesLogic; +import teammates.sqllogic.core.FeedbackSessionLogsLogic; import teammates.sqllogic.core.FeedbackSessionsLogic; import teammates.sqllogic.core.NotificationsLogic; import teammates.sqllogic.core.UsageStatisticsLogic; @@ -42,6 +43,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.Section; @@ -69,6 +71,7 @@ public class Logic { final FeedbackResponsesLogic feedbackResponsesLogic = FeedbackResponsesLogic.inst(); final FeedbackResponseCommentsLogic feedbackResponseCommentsLogic = FeedbackResponseCommentsLogic.inst(); final FeedbackSessionsLogic feedbackSessionsLogic = FeedbackSessionsLogic.inst(); + final FeedbackSessionLogsLogic feedbackSessionLogsLogic = FeedbackSessionLogsLogic.inst(); final UsageStatisticsLogic usageStatisticsLogic = UsageStatisticsLogic.inst(); final UsersLogic usersLogic = UsersLogic.inst(); final NotificationsLogic notificationsLogic = NotificationsLogic.inst(); @@ -515,6 +518,15 @@ public FeedbackSession getFeedbackSession(String feedbackSessionName, String cou return feedbackSessionsLogic.getFeedbackSession(feedbackSessionName, courseId); } + /** + * Gets a feedback session reference. + * + * @return Returns a proxy for the feedback session. + */ + public FeedbackSession getFeedbackSessionReference(UUID id) { + return feedbackSessionsLogic.getFeedbackSessionReference(id); + } + /** * Gets a feedback session from the recycle bin. * @@ -996,6 +1008,16 @@ public Student getStudent(UUID id) { return usersLogic.getStudent(id); } + /** + * Gets student reference associated with {@code id}. + * + * @param id Id of Student. + * @return Returns a proxy for the Student. + */ + public Student getStudentReference(UUID id) { + return usersLogic.getStudentReference(id); + } + /** * Gets student associated with {@code courseId} and {@code email}. */ @@ -1668,4 +1690,25 @@ public List getFeedbackSessionsClosingWithinTimeLimit() { public List getFeedbackSessionsOpeningWithinTimeLimit() { return feedbackSessionsLogic.getFeedbackSessionsOpeningWithinTimeLimit(); } + + /** + * Create feedback session logs. + */ + public void createFeedbackSessionLogs(List feedbackSessionLogs) { + feedbackSessionLogsLogic.createFeedbackSessionLogs(feedbackSessionLogs); + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by + * ascending timestamp. Logs with the same timestamp will be ordered by the + * student's email. + * + * @param studentId Can be null + * @param feedbackSessionId Can be null + */ + public List getOrderedFeedbackSessionLogs(String courseId, UUID studentId, + UUID feedbackSessionId, Instant startTime, Instant endTime) { + return feedbackSessionLogsLogic.getOrderedFeedbackSessionLogs(courseId, studentId, feedbackSessionId, startTime, + endTime); + } } diff --git a/src/main/java/teammates/sqllogic/core/DataBundleLogic.java b/src/main/java/teammates/sqllogic/core/DataBundleLogic.java index 0c7c9f66311..0bb79e0ff72 100644 --- a/src/main/java/teammates/sqllogic/core/DataBundleLogic.java +++ b/src/main/java/teammates/sqllogic/core/DataBundleLogic.java @@ -1,5 +1,6 @@ package teammates.sqllogic.core; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -19,6 +20,7 @@ import teammates.storage.sqlentity.FeedbackResponse; import teammates.storage.sqlentity.FeedbackResponseComment; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Notification; import teammates.storage.sqlentity.ReadNotification; @@ -41,6 +43,7 @@ public final class DataBundleLogic { private CoursesLogic coursesLogic; private DeadlineExtensionsLogic deadlineExtensionsLogic; private FeedbackSessionsLogic fsLogic; + private FeedbackSessionLogsLogic fslLogic; private FeedbackQuestionsLogic fqLogic; private FeedbackResponsesLogic frLogic; private FeedbackResponseCommentsLogic frcLogic; @@ -56,16 +59,15 @@ public static DataBundleLogic inst() { } void initLogicDependencies(AccountsLogic accountsLogic, AccountRequestsLogic accountRequestsLogic, - CoursesLogic coursesLogic, - DeadlineExtensionsLogic deadlineExtensionsLogic, FeedbackSessionsLogic fsLogic, - FeedbackQuestionsLogic fqLogic, FeedbackResponsesLogic frLogic, - FeedbackResponseCommentsLogic frcLogic, - NotificationsLogic notificationsLogic, UsersLogic usersLogic) { + CoursesLogic coursesLogic, DeadlineExtensionsLogic deadlineExtensionsLogic, FeedbackSessionsLogic fsLogic, + FeedbackSessionLogsLogic fslLogic, FeedbackQuestionsLogic fqLogic, FeedbackResponsesLogic frLogic, + FeedbackResponseCommentsLogic frcLogic, NotificationsLogic notificationsLogic, UsersLogic usersLogic) { this.accountsLogic = accountsLogic; this.accountRequestsLogic = accountRequestsLogic; this.coursesLogic = coursesLogic; this.deadlineExtensionsLogic = deadlineExtensionsLogic; this.fsLogic = fsLogic; + this.fslLogic = fslLogic; this.fqLogic = fqLogic; this.frLogic = frLogic; this.frcLogic = frcLogic; @@ -97,6 +99,7 @@ public static SqlDataBundle deserializeDataBundle(String jsonString) { Collection instructors = dataBundle.instructors.values(); Collection students = dataBundle.students.values(); Collection sessions = dataBundle.feedbackSessions.values(); + Collection sessionLogs = dataBundle.feedbackSessionLogs.values(); Collection questions = dataBundle.feedbackQuestions.values(); Collection responses = dataBundle.feedbackResponses.values(); Collection responseComments = dataBundle.feedbackResponseComments.values(); @@ -215,6 +218,14 @@ public static SqlDataBundle deserializeDataBundle(String jsonString) { student.generateNewRegistrationKey(); } + for (FeedbackSessionLog log : sessionLogs) { + log.setId(UUID.randomUUID()); + FeedbackSession fs = sessionsMap.get(log.getFeedbackSession().getId()); + log.setFeedbackSession(fs); + Student student = (Student) usersMap.get(log.getStudent().getId()); + log.setStudent(student); + } + for (Notification notification : notifications) { UUID placeholderId = notification.getId(); notification.setId(UUID.randomUUID()); @@ -264,6 +275,7 @@ public SqlDataBundle persistDataBundle(SqlDataBundle dataBundle) Collection instructors = dataBundle.instructors.values(); Collection students = dataBundle.students.values(); Collection sessions = dataBundle.feedbackSessions.values(); + Collection sessionLogs = dataBundle.feedbackSessionLogs.values(); Collection questions = dataBundle.feedbackQuestions.values(); Collection responses = dataBundle.feedbackResponses.values(); Collection responseComments = dataBundle.feedbackResponseComments.values(); @@ -320,6 +332,8 @@ public SqlDataBundle persistDataBundle(SqlDataBundle dataBundle) usersLogic.createStudent(student); } + fslLogic.createFeedbackSessionLogs(new ArrayList<>(sessionLogs)); + for (ReadNotification readNotification : readNotifications) { accountsLogic.updateReadNotifications(readNotification.getAccount().getGoogleId(), readNotification.getNotification().getId(), readNotification.getNotification().getEndTime()); diff --git a/src/main/java/teammates/sqllogic/core/FeedbackSessionLogsLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackSessionLogsLogic.java new file mode 100644 index 00000000000..8ea0c4f3fe5 --- /dev/null +++ b/src/main/java/teammates/sqllogic/core/FeedbackSessionLogsLogic.java @@ -0,0 +1,67 @@ +package teammates.sqllogic.core; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import org.hibernate.ObjectNotFoundException; + +import teammates.common.util.Logger; +import teammates.storage.sqlapi.FeedbackSessionLogsDb; +import teammates.storage.sqlentity.FeedbackSessionLog; + +/** + * Handles operations related to feedback sessions. + * + * @see FeedbackSessionLog + * @see FeedbackSessionLogsDb + */ +public final class FeedbackSessionLogsLogic { + + private static final Logger log = Logger.getLogger(); + + private static final FeedbackSessionLogsLogic instance = new FeedbackSessionLogsLogic(); + + private static final String ERROR_FAILED_TO_CREATE_LOG = "Failed to create session activity log"; + + private FeedbackSessionLogsDb fslDb; + + private FeedbackSessionLogsLogic() { + // prevent initialization + } + + public static FeedbackSessionLogsLogic inst() { + return instance; + } + + void initLogicDependencies(FeedbackSessionLogsDb fslDb) { + this.fslDb = fslDb; + } + + /** + * Creates feedback session logs. + */ + public void createFeedbackSessionLogs(List fsLogs) { + for (FeedbackSessionLog fsLog : fsLogs) { + try { + fslDb.createFeedbackSessionLog(fsLog); + } catch (ObjectNotFoundException e) { + log.severe(String.format(ERROR_FAILED_TO_CREATE_LOG), e); + } + } + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by + * ascending timestamp. Logs with the same timestamp will be ordered by the + * student's email. + * + * @param studentId Can be null + * @param feedbackSessionId Can be null + */ + public List getOrderedFeedbackSessionLogs(String courseId, UUID studentId, + UUID feedbackSessionId, Instant startTime, Instant endTime) { + return fslDb.getOrderedFeedbackSessionLogs(courseId, studentId, feedbackSessionId, startTime, + endTime); + } +} diff --git a/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java b/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java index 84ea61b2a0c..a707990d083 100644 --- a/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java +++ b/src/main/java/teammates/sqllogic/core/FeedbackSessionsLogic.java @@ -87,6 +87,16 @@ public FeedbackSession getFeedbackSession(String feedbackSessionName, String cou return fsDb.getFeedbackSession(feedbackSessionName, courseId); } + /** + * Gets a feedback session reference. + * + * @return Returns a proxy for the feedback session. + */ + public FeedbackSession getFeedbackSessionReference(UUID id) { + assert id != null; + return fsDb.getFeedbackSessionReference(id); + } + /** * Gets all feedback sessions of a course, except those that are soft-deleted. */ diff --git a/src/main/java/teammates/sqllogic/core/LogicStarter.java b/src/main/java/teammates/sqllogic/core/LogicStarter.java index b474cac8980..ba67aff710a 100644 --- a/src/main/java/teammates/sqllogic/core/LogicStarter.java +++ b/src/main/java/teammates/sqllogic/core/LogicStarter.java @@ -11,6 +11,7 @@ import teammates.storage.sqlapi.FeedbackQuestionsDb; import teammates.storage.sqlapi.FeedbackResponseCommentsDb; import teammates.storage.sqlapi.FeedbackResponsesDb; +import teammates.storage.sqlapi.FeedbackSessionLogsDb; import teammates.storage.sqlapi.FeedbackSessionsDb; import teammates.storage.sqlapi.NotificationsDb; import teammates.storage.sqlapi.UsageStatisticsDb; @@ -33,6 +34,7 @@ public static void initializeDependencies() { DataBundleLogic dataBundleLogic = DataBundleLogic.inst(); DeadlineExtensionsLogic deadlineExtensionsLogic = DeadlineExtensionsLogic.inst(); FeedbackSessionsLogic fsLogic = FeedbackSessionsLogic.inst(); + FeedbackSessionLogsLogic fslLogic = FeedbackSessionLogsLogic.inst(); FeedbackResponsesLogic frLogic = FeedbackResponsesLogic.inst(); FeedbackResponseCommentsLogic frcLogic = FeedbackResponseCommentsLogic.inst(); FeedbackQuestionsLogic fqLogic = FeedbackQuestionsLogic.inst(); @@ -44,10 +46,11 @@ public static void initializeDependencies() { accountsLogic.initLogicDependencies(AccountsDb.inst(), notificationsLogic, usersLogic, coursesLogic); coursesLogic.initLogicDependencies(CoursesDb.inst(), fsLogic, usersLogic); dataBundleLogic.initLogicDependencies(accountsLogic, accountRequestsLogic, coursesLogic, - deadlineExtensionsLogic, fsLogic, fqLogic, frLogic, frcLogic, + deadlineExtensionsLogic, fsLogic, fslLogic, fqLogic, frLogic, frcLogic, notificationsLogic, usersLogic); deadlineExtensionsLogic.initLogicDependencies(DeadlineExtensionsDb.inst(), fsLogic); fsLogic.initLogicDependencies(FeedbackSessionsDb.inst(), coursesLogic, frLogic, fqLogic, usersLogic); + fslLogic.initLogicDependencies(FeedbackSessionLogsDb.inst()); frLogic.initLogicDependencies(FeedbackResponsesDb.inst(), usersLogic, fqLogic, frcLogic); frcLogic.initLogicDependencies(FeedbackResponseCommentsDb.inst()); fqLogic.initLogicDependencies(FeedbackQuestionsDb.inst(), coursesLogic, frLogic, usersLogic, fsLogic); diff --git a/src/main/java/teammates/sqllogic/core/UsersLogic.java b/src/main/java/teammates/sqllogic/core/UsersLogic.java index e0486616a69..bfac68a3f36 100644 --- a/src/main/java/teammates/sqllogic/core/UsersLogic.java +++ b/src/main/java/teammates/sqllogic/core/UsersLogic.java @@ -482,6 +482,18 @@ public Student getStudent(UUID id) { return usersDb.getStudent(id); } + /** + * Gets student reference associated with {@code id}. + * + * @param id Id of Student. + * @return Returns a proxy for the Student. + */ + public Student getStudentReference(UUID id) { + assert id != null; + + return usersDb.getStudentReference(id); + } + /** * Gets the student with the specified email. */ diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackSessionLogsDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackSessionLogsDb.java new file mode 100644 index 00000000000..c9f6af098ff --- /dev/null +++ b/src/main/java/teammates/storage/sqlapi/FeedbackSessionLogsDb.java @@ -0,0 +1,86 @@ +package teammates.storage.sqlapi; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +/** + * Handles CRUD operations for feedback session logs. + * + * @see FeedbackSessionLog + */ +public final class FeedbackSessionLogsDb extends EntitiesDb { + + private static final FeedbackSessionLogsDb instance = new FeedbackSessionLogsDb(); + + private FeedbackSessionLogsDb() { + // prevent initialization + } + + public static FeedbackSessionLogsDb inst() { + return instance; + } + + /** + * Gets the feedback session logs as filtered by the given parameters ordered by + * ascending timestamp. Logs with the same timestamp will be ordered by the + * student's email. + * + * @param studentId Can be null + * @param feedbackSessionId Can be null + */ + public List getOrderedFeedbackSessionLogs(String courseId, UUID studentId, + UUID feedbackSessionId, Instant startTime, Instant endTime) { + + assert courseId != null; + assert startTime != null; + assert endTime != null; + + CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(FeedbackSessionLog.class); + Root root = cr.from(FeedbackSessionLog.class); + Join feedbackSessionJoin = root.join("feedbackSession"); + Join studentJoin = root.join("student"); + + List predicates = new ArrayList<>(); + + if (studentId != null) { + predicates.add(cb.equal(studentJoin.get("id"), studentId)); + } + + if (feedbackSessionId != null) { + predicates.add(cb.equal(feedbackSessionJoin.get("id"), feedbackSessionId)); + } + + predicates.add(cb.equal(feedbackSessionJoin.get("course").get("id"), courseId)); + predicates.add(cb.greaterThanOrEqualTo(root.get("timestamp"), startTime)); + predicates.add(cb.lessThan(root.get("timestamp"), endTime)); + + cr.select(root).where(predicates.toArray(new Predicate[0])).orderBy(cb.asc(root.get("timestamp")), + cb.asc(studentJoin.get("email"))); + return HibernateUtil.createQuery(cr).getResultList(); + } + + /** + * Creates feedback session logs. + */ + public FeedbackSessionLog createFeedbackSessionLog(FeedbackSessionLog log) { + assert log != null; + + persist(log); + + return log; + } +} diff --git a/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java b/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java index 389407e9f28..078d453da2c 100644 --- a/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java +++ b/src/main/java/teammates/storage/sqlapi/FeedbackSessionsDb.java @@ -65,6 +65,17 @@ public FeedbackSession getFeedbackSession(String feedbackSessionName, String cou return HibernateUtil.createQuery(cq).getResultStream().findFirst().orElse(null); } + /** + * Gets a feedback session reference. + * + * @return Returns a proxy for the feedback session. + */ + public FeedbackSession getFeedbackSessionReference(UUID id) { + assert id != null; + + return HibernateUtil.getReference(FeedbackSession.class, id); + } + /** * Gets a soft-deleted feedback session. * diff --git a/src/main/java/teammates/storage/sqlapi/UsersDb.java b/src/main/java/teammates/storage/sqlapi/UsersDb.java index 5d3b8571071..3621f5c6224 100644 --- a/src/main/java/teammates/storage/sqlapi/UsersDb.java +++ b/src/main/java/teammates/storage/sqlapi/UsersDb.java @@ -146,6 +146,15 @@ public Student getStudent(UUID id) { return HibernateUtil.get(Student.class, id); } + /** + * Gets a student reference by its {@code id}. + */ + public Student getStudentReference(UUID id) { + assert id != null; + + return HibernateUtil.getReference(Student.class, id); + } + /** * Gets a student by {@code regKey}. */ diff --git a/src/main/java/teammates/storage/sqlentity/FeedbackSessionLog.java b/src/main/java/teammates/storage/sqlentity/FeedbackSessionLog.java new file mode 100644 index 00000000000..15daeb607b5 --- /dev/null +++ b/src/main/java/teammates/storage/sqlentity/FeedbackSessionLog.java @@ -0,0 +1,135 @@ +package teammates.storage.sqlentity; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import teammates.common.datatransfer.logs.FeedbackSessionLogType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +/** + * Represents a feedback session log. + */ +@Entity +@Table(name = "FeedbackSessionLogs") +public class FeedbackSessionLog extends BaseEntity { + @Id + private UUID id; + + @ManyToOne + @JoinColumn(name = "studentId") + @NotFound(action = NotFoundAction.IGNORE) + @OnDelete(action = OnDeleteAction.CASCADE) + private Student student; + + @ManyToOne + @JoinColumn(name = "sessionId") + @NotFound(action = NotFoundAction.IGNORE) + @OnDelete(action = OnDeleteAction.CASCADE) + private FeedbackSession feedbackSession; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private FeedbackSessionLogType feedbackSessionLogType; + + @Column(nullable = false) + private Instant timestamp; + + protected FeedbackSessionLog() { + // required by Hibernate + } + + public FeedbackSessionLog(Student student, FeedbackSession feedbackSession, + FeedbackSessionLogType feedbackSessionLogType, Instant timestamp) { + this.setId(UUID.randomUUID()); + this.student = student; + this.feedbackSession = feedbackSession; + this.feedbackSessionLogType = feedbackSessionLogType; + this.timestamp = timestamp; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public Student getStudent() { + return student; + } + + public void setStudent(Student student) { + this.student = student; + } + + public FeedbackSession getFeedbackSession() { + return feedbackSession; + } + + public void setFeedbackSession(FeedbackSession feedbackSession) { + this.feedbackSession = feedbackSession; + } + + public FeedbackSessionLogType getFeedbackSessionLogType() { + return feedbackSessionLogType; + } + + public void setFeedbackSessionLogType(FeedbackSessionLogType feedbackSessionLogType) { + this.feedbackSessionLogType = feedbackSessionLogType; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "FeedbackSessionLog [id=" + id + ", student=" + student + ", feedbackSession=" + feedbackSession + + ", feedbackSessionLogType=" + feedbackSessionLogType.getLabel() + ", timestamp=" + timestamp + "]"; + } + + @Override + public int hashCode() { + return this.getId().hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } else if (this == other) { + return true; + } else if (this.getClass() == other.getClass()) { + FeedbackSessionLog otherFeedbackSessionLog = (FeedbackSessionLog) other; + return Objects.equals(this.getId(), otherFeedbackSessionLog.getId()); + } else { + return false; + } + } + + @Override + public List getInvalidityInfo() { + return new ArrayList<>(); + } +} diff --git a/src/main/java/teammates/ui/constants/ApiConst.java b/src/main/java/teammates/ui/constants/ApiConst.java index ce946c8f3e6..2a411e7b1bd 100644 --- a/src/main/java/teammates/ui/constants/ApiConst.java +++ b/src/main/java/teammates/ui/constants/ApiConst.java @@ -28,7 +28,9 @@ public enum ApiConst { RANK_RECIPIENTS_ANSWER_NOT_SUBMITTED(Const.POINTS_NOT_SUBMITTED), NO_VALUE(Const.POINTS_NO_VALUE), LOGS_RETENTION_PERIOD(Const.LOGS_RETENTION_PERIOD.toDays()), - SEARCH_QUERY_SIZE_LIMIT(Const.SEARCH_QUERY_SIZE_LIMIT); + SEARCH_QUERY_SIZE_LIMIT(Const.SEARCH_QUERY_SIZE_LIMIT), + STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL(Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes()); + // CHECKSTYLE.ON:JavadocVariable private final Object value; diff --git a/src/main/java/teammates/ui/output/CourseData.java b/src/main/java/teammates/ui/output/CourseData.java index b2ce6334483..89b751de3fd 100644 --- a/src/main/java/teammates/ui/output/CourseData.java +++ b/src/main/java/teammates/ui/output/CourseData.java @@ -15,6 +15,8 @@ public class CourseData extends ApiOutput { private final String courseName; private final String timeZone; private final String institute; + @Nullable + private final Boolean isMigrated; private long creationTimestamp; private long deletionTimestamp; @Nullable @@ -29,6 +31,7 @@ public CourseData(CourseAttributes courseAttributes) { if (courseAttributes.getDeletedAt() != null) { this.deletionTimestamp = courseAttributes.getDeletedAt().toEpochMilli(); } + this.isMigrated = false; } public CourseData(Course course) { @@ -40,6 +43,7 @@ public CourseData(Course course) { if (course.getDeletedAt() != null) { this.deletionTimestamp = course.getDeletedAt().toEpochMilli(); } + this.isMigrated = true; } public String getCourseId() { @@ -66,6 +70,10 @@ public long getDeletionTimestamp() { return deletionTimestamp; } + public Boolean getIsMigrated() { + return isMigrated; + } + public InstructorPermissionSet getPrivileges() { return privileges; } diff --git a/src/main/java/teammates/ui/output/FeedbackSessionData.java b/src/main/java/teammates/ui/output/FeedbackSessionData.java index 9b529f1b5b2..b5aef354d88 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionData.java @@ -3,6 +3,7 @@ import java.time.Instant; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -20,6 +21,10 @@ * The API output format of {@link FeedbackSessionAttributes}. */ public class FeedbackSessionData extends ApiOutput { + + @Nullable + private final UUID feedbackSessionId; + private final String courseId; private final String timeZone; private final String feedbackSessionName; @@ -60,6 +65,7 @@ public class FeedbackSessionData extends ApiOutput { public FeedbackSessionData(FeedbackSessionAttributes feedbackSessionAttributes) { String timeZone = feedbackSessionAttributes.getTimeZone(); + this.feedbackSessionId = null; this.courseId = feedbackSessionAttributes.getCourseId(); this.timeZone = timeZone; this.feedbackSessionName = feedbackSessionAttributes.getFeedbackSessionName(); @@ -148,6 +154,7 @@ public FeedbackSessionData(FeedbackSession feedbackSession) { assert feedbackSession != null; assert feedbackSession.getCourse() != null; String timeZone = feedbackSession.getCourse().getTimeZone(); + this.feedbackSessionId = feedbackSession.getId(); this.courseId = feedbackSession.getCourse().getId(); this.timeZone = timeZone; this.feedbackSessionName = feedbackSession.getName(); @@ -252,6 +259,10 @@ public FeedbackSessionData(FeedbackSession feedbackSession, Instant extendedDead } } + public UUID getFeedbackSessionId() { + return feedbackSessionId; + } + public String getCourseId() { return courseId; } diff --git a/src/main/java/teammates/ui/output/FeedbackSessionLogData.java b/src/main/java/teammates/ui/output/FeedbackSessionLogData.java index 0825b0b5894..7afc398f0bf 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionLogData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionLogData.java @@ -8,6 +8,7 @@ import teammates.common.datatransfer.attributes.FeedbackSessionAttributes; import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Student; /** @@ -17,19 +18,24 @@ public class FeedbackSessionLogData { private final FeedbackSessionData feedbackSessionData; private final List feedbackSessionLogEntries; - // Remove generic types after migration is done (i.e. can just use FeedbackSession and Student) - public FeedbackSessionLogData(S feedbackSession, List logEntries, + // Remove generic types after migration is done (i.e. can just use FeedbackSession, Student, FeedbackSessionLog) + public FeedbackSessionLogData(S feedbackSession, List logEntries, Map studentsMap) { if (feedbackSession instanceof FeedbackSessionAttributes) { FeedbackSessionAttributes fs = (FeedbackSessionAttributes) feedbackSession; FeedbackSessionData fsData = new FeedbackSessionData(fs); List fsLogEntryDatas = logEntries.stream() .map(log -> { - T student = studentsMap.get(log.getStudentEmail()); - if (student instanceof StudentAttributes) { - return new FeedbackSessionLogEntryData(log, (StudentAttributes) student); + if (log instanceof FeedbackSessionLogEntry) { + FeedbackSessionLogEntry convertedLog = (FeedbackSessionLogEntry) log; + T student = studentsMap.get(convertedLog.getStudentEmail()); + if (student instanceof StudentAttributes) { + return new FeedbackSessionLogEntryData(convertedLog, (StudentAttributes) student); + } else { + throw new IllegalArgumentException("Invalid student type"); + } } else { - throw new IllegalArgumentException("Invalid student type"); + throw new IllegalArgumentException("Invalid log type"); } }) .collect(Collectors.toList()); @@ -40,11 +46,16 @@ public FeedbackSessionLogData(S feedbackSession, List fsLogEntryDatas = logEntries.stream() .map(log -> { - T student = studentsMap.get(log.getStudentEmail()); - if (student instanceof Student) { - return new FeedbackSessionLogEntryData(log, (Student) student); + if (log instanceof FeedbackSessionLog) { + FeedbackSessionLog convertedLog = (FeedbackSessionLog) log; + T student = studentsMap.get(convertedLog.getStudent().getEmail()); + if (student instanceof Student) { + return new FeedbackSessionLogEntryData(convertedLog, (Student) student); + } else { + throw new IllegalArgumentException("Invalid student type"); + } } else { - throw new IllegalArgumentException("Invalid student type"); + throw new IllegalArgumentException("Invalid log type"); } }) .collect(Collectors.toList()); diff --git a/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java b/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java index a70eaa7b505..99669d10e33 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionLogEntryData.java @@ -3,6 +3,7 @@ import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Student; /** @@ -22,10 +23,10 @@ public FeedbackSessionLogEntryData(FeedbackSessionLogEntry logEntry, StudentAttr this.timestamp = timestamp; } - public FeedbackSessionLogEntryData(FeedbackSessionLogEntry logEntry, Student student) { + public FeedbackSessionLogEntryData(FeedbackSessionLog logEntry, Student student) { StudentData studentData = new StudentData(student); - FeedbackSessionLogType logType = FeedbackSessionLogType.valueOfLabel(logEntry.getFeedbackSessionLogType()); - long timestamp = logEntry.getTimestamp(); + FeedbackSessionLogType logType = logEntry.getFeedbackSessionLogType(); + long timestamp = logEntry.getTimestamp().toEpochMilli(); this.studentData = studentData; this.feedbackSessionLogType = logType; this.timestamp = timestamp; diff --git a/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java b/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java index 3926e252817..b6f722dc770 100644 --- a/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java +++ b/src/main/java/teammates/ui/output/FeedbackSessionLogsData.java @@ -4,8 +4,6 @@ import java.util.Map; import java.util.stream.Collectors; -import teammates.common.datatransfer.FeedbackSessionLogEntry; - /** * The API output format for logs on all feedback sessions in a course. */ @@ -13,13 +11,13 @@ public class FeedbackSessionLogsData extends ApiOutput { private final List feedbackSessionLogs; - // Remove generic types after migration is done (i.e. can just use FeedbackSession and Student) - public FeedbackSessionLogsData(Map> groupedEntries, + // Remove generic types after migration is done (i.e. can just use FeedbackSession and Student, FeedbackSessionLog) + public FeedbackSessionLogsData(Map> groupedEntries, Map studentsMap, Map sessionsMap) { this.feedbackSessionLogs = groupedEntries.entrySet().stream() .map(entry -> { T feedbackSession = sessionsMap.get(entry.getKey()); - List logEntries = entry.getValue(); + List logEntries = entry.getValue(); return new FeedbackSessionLogData(feedbackSession, logEntries, studentsMap); }) .collect(Collectors.toList()); diff --git a/src/main/java/teammates/ui/output/StudentData.java b/src/main/java/teammates/ui/output/StudentData.java index 933b9e9d025..21138ae9026 100644 --- a/src/main/java/teammates/ui/output/StudentData.java +++ b/src/main/java/teammates/ui/output/StudentData.java @@ -1,5 +1,7 @@ package teammates.ui.output; +import java.util.UUID; + import javax.annotation.Nullable; import teammates.common.datatransfer.attributes.StudentAttributes; @@ -10,6 +12,9 @@ */ public class StudentData extends ApiOutput { + @Nullable + private final UUID studentId; + private final String email; private final String courseId; @@ -29,6 +34,7 @@ public class StudentData extends ApiOutput { private final String sectionName; public StudentData(StudentAttributes studentAttributes) { + this.studentId = null; this.email = studentAttributes.getEmail(); this.courseId = studentAttributes.getCourse(); this.name = studentAttributes.getName(); @@ -39,6 +45,7 @@ public StudentData(StudentAttributes studentAttributes) { } public StudentData(Student student) { + this.studentId = student.getId(); this.email = student.getEmail(); this.courseId = student.getCourseId(); this.name = student.getName(); @@ -48,6 +55,10 @@ public StudentData(Student student) { this.sectionName = student.getSectionName(); } + public UUID getStudentId() { + return studentId; + } + public String getEmail() { return email; } diff --git a/src/main/java/teammates/ui/webapi/ActionFactory.java b/src/main/java/teammates/ui/webapi/ActionFactory.java index ae834448b7a..4bcd0202673 100644 --- a/src/main/java/teammates/ui/webapi/ActionFactory.java +++ b/src/main/java/teammates/ui/webapi/ActionFactory.java @@ -154,6 +154,7 @@ public final class ActionFactory { map(CronJobURIs.AUTOMATED_FEEDBACK_OPENING_SOON_REMINDERS, GET, FeedbackSessionOpeningSoonRemindersAction.class); map(CronJobURIs.AUTOMATED_USAGE_STATISTICS_COLLECTION, GET, CalculateUsageStatisticsAction.class); + map(CronJobURIs.AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING, GET, UpdateFeedbackSessionLogsAction.class); // Task queue workers; use POST request // Reference: https://cloud.google.com/tasks/docs/creating-appengine-tasks diff --git a/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java b/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java index cf77994aa9f..5ea725da87b 100644 --- a/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java +++ b/src/main/java/teammates/ui/webapi/CreateFeedbackSessionLogAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import teammates.common.datatransfer.logs.FeedbackSessionAuditLogDetails; import teammates.common.datatransfer.logs.FeedbackSessionLogType; import teammates.common.util.Const; @@ -8,7 +10,7 @@ /** * Action: creates a feedback session log for the purposes of tracking and auditing. */ -class CreateFeedbackSessionLogAction extends Action { +public class CreateFeedbackSessionLogAction extends Action { private static final Logger log = Logger.getLogger(); @@ -33,10 +35,8 @@ public JsonResult execute() { String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); String fsName = getNonNullRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); String studentEmail = getNonNullRequestParamValue(Const.ParamsNames.STUDENT_EMAIL); - // Skip rigorous validations to avoid incurring extra db reads and to keep the endpoint light - - // Necessary to assist local testing. For production usage, this will be a no-op. - logsProcessor.createFeedbackSessionLog(courseId, studentEmail, fsName, fslType); + // Skip rigorous validations to avoid incurring extra db reads and to keep the endpoint + // light FeedbackSessionAuditLogDetails details = new FeedbackSessionAuditLogDetails(); details.setCourseId(courseId); @@ -44,6 +44,20 @@ public JsonResult execute() { details.setStudentEmail(studentEmail); details.setAccessType(fslType); + if (isCourseMigrated(courseId)) { + UUID studentId = getUuidRequestParamValue(Const.ParamsNames.STUDENT_SQL_ID); + UUID fsId = getUuidRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_ID); + + details.setStudentId(studentId.toString()); + details.setFeedbackSessionId(fsId.toString()); + + // Necessary to assist local testing. For production usage, this will be a no-op. + logsProcessor.createFeedbackSessionLog(courseId, studentId, fsId, fslType); + } else { + // Necessary to assist local testing. For production usage, this will be a no-op. + logsProcessor.createFeedbackSessionLog(courseId, studentEmail, fsName, fslType); + } + log.event("Feedback session audit event: " + fslType, details); return new JsonResult("Successful"); diff --git a/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java b/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java index 72332d439e5..720ec6e726b 100644 --- a/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java +++ b/src/main/java/teammates/ui/webapi/GetFeedbackSessionLogsAction.java @@ -1,10 +1,12 @@ package teammates.ui.webapi; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import teammates.common.datatransfer.FeedbackSessionLogEntry; @@ -17,6 +19,7 @@ import teammates.common.util.TimeHelper; import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; import teammates.storage.sqlentity.Instructor; import teammates.storage.sqlentity.Student; import teammates.ui.output.FeedbackSessionLogsData; @@ -65,36 +68,6 @@ void checkSpecificAccessControl() throws UnauthorizedAccessException { @Override public JsonResult execute() { - String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); - String email = getRequestParamValue(Const.ParamsNames.STUDENT_EMAIL); - String feedbackSessionName = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); - - if (isCourseMigrated(courseId)) { - if (sqlLogic.getCourse(courseId) == null) { - throw new EntityNotFoundException("Course not found"); - } - - if (email != null && sqlLogic.getStudentForEmail(courseId, email) == null) { - throw new EntityNotFoundException("Student not found"); - } - - if (feedbackSessionName != null && sqlLogic.getFeedbackSession(feedbackSessionName, courseId) == null) { - throw new EntityNotFoundException("Feedback session not found"); - } - } else { - if (logic.getCourse(courseId) == null) { - throw new EntityNotFoundException("Course not found"); - } - - if (email != null && logic.getStudentForEmail(courseId, email) == null) { - throw new EntityNotFoundException("Student not found"); - } - - if (feedbackSessionName != null && logic.getFeedbackSession(feedbackSessionName, courseId) == null) { - throw new EntityNotFoundException("Feedback session not found"); - } - } - String fslTypes = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE); List convertedFslTypes = new ArrayList<>(); if (fslTypes != null) { @@ -126,51 +99,92 @@ public JsonResult execute() { throw new InvalidHttpParameterException("The end time should be after the start time."); } - long earliestSearchTime = TimeHelper.getInstantDaysOffsetBeforeNow(Const.LOGS_RETENTION_PERIOD.toDays()) - .toEpochMilli(); - if (startTime < earliestSearchTime) { - throw new InvalidHttpParameterException( - "The earliest date you can search for is " + Const.LOGS_RETENTION_PERIOD.toDays() + " days before today." - ); - } + String courseId = getNonNullRequestParamValue(Const.ParamsNames.COURSE_ID); - List fsLogEntries = - logsProcessor.getFeedbackSessionLogs(courseId, email, startTime, endTime, feedbackSessionName); + if (!isCourseMigrated(courseId)) { + long earliestSearchTime = TimeHelper.getInstantDaysOffsetBeforeNow(Const.LOGS_RETENTION_PERIOD.toDays()) + .toEpochMilli(); + if (startTime < earliestSearchTime) { + throw new InvalidHttpParameterException("The earliest date you can search for is " + + Const.LOGS_RETENTION_PERIOD.toDays() + " days before today."); + } + } if (isCourseMigrated(courseId)) { + UUID studentId = null; + UUID feedbackSessionId = null; + String studentIdString = getRequestParamValue(Const.ParamsNames.STUDENT_SQL_ID); + String feedbackSessionIdString = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_ID); + + if (studentIdString != null) { + studentId = getUuidFromString(Const.ParamsNames.STUDENT_SQL_ID, studentIdString); + } + + if (feedbackSessionIdString != null) { + feedbackSessionId = getUuidFromString(Const.ParamsNames.FEEDBACK_SESSION_ID, feedbackSessionIdString); + } + + if (sqlLogic.getCourse(courseId) == null) { + throw new EntityNotFoundException("Course not found"); + } + + if (studentId != null && sqlLogic.getStudent(studentId) == null) { + throw new EntityNotFoundException("Student not found"); + } + + if (feedbackSessionId != null && sqlLogic.getFeedbackSession(feedbackSessionId) == null) { + throw new EntityNotFoundException("Feedback session not found"); + } + + List fsLogEntries = sqlLogic.getOrderedFeedbackSessionLogs(courseId, studentId, + feedbackSessionId, Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime)); Map studentsMap = new HashMap<>(); Map sessionsMap = new HashMap<>(); List feedbackSessions = sqlLogic.getFeedbackSessionsForCourse(courseId); feedbackSessions.forEach(fs -> sessionsMap.put(fs.getName(), fs)); fsLogEntries = fsLogEntries.stream().filter(logEntry -> { - String logType = logEntry.getFeedbackSessionLogType(); - FeedbackSessionLogType convertedLogType = FeedbackSessionLogType.valueOfLabel(logType); - if (convertedLogType == null || fslTypes != null && !convertedFslTypes.contains(convertedLogType)) { + FeedbackSessionLogType logType = logEntry.getFeedbackSessionLogType(); + if (logType == null || fslTypes != null && !convertedFslTypes.contains(logType)) { // If the feedback session log type retrieved from the log is invalid // or not the type being queried, ignore the log return false; } - if (!studentsMap.containsKey(logEntry.getStudentEmail())) { - Student student = sqlLogic.getStudentForEmail(courseId, logEntry.getStudentEmail()); + if (!studentsMap.containsKey(logEntry.getStudent().getEmail())) { + Student student = sqlLogic.getStudent(logEntry.getStudent().getId()); if (student == null) { // If the student email retrieved from the log is invalid, ignore the log return false; } - studentsMap.put(logEntry.getStudentEmail(), student); + studentsMap.put(student.getEmail(), student); } // If the feedback session retrieved from the log is invalid, ignore the log - return sessionsMap.containsKey(logEntry.getFeedbackSessionName()); + return sessionsMap.containsKey(logEntry.getFeedbackSession().getName()); }).collect(Collectors.toList()); - Map> groupedEntries = - groupFeedbackSessionLogEntries(fsLogEntries); + Map> groupedEntries = groupFeedbackSessionLogs(fsLogEntries); feedbackSessions.forEach(fs -> groupedEntries.putIfAbsent(fs.getName(), new ArrayList<>())); FeedbackSessionLogsData fslData = new FeedbackSessionLogsData(groupedEntries, studentsMap, sessionsMap); return new JsonResult(fslData); } else { + if (logic.getCourse(courseId) == null) { + throw new EntityNotFoundException("Course not found"); + } + + String email = getRequestParamValue(Const.ParamsNames.STUDENT_EMAIL); + if (email != null && logic.getStudentForEmail(courseId, email) == null) { + throw new EntityNotFoundException("Student not found"); + } + + String feedbackSessionName = getRequestParamValue(Const.ParamsNames.FEEDBACK_SESSION_NAME); + if (feedbackSessionName != null && logic.getFeedbackSession(feedbackSessionName, courseId) == null) { + throw new EntityNotFoundException("Feedback session not found"); + } + + List fsLogEntries = + logsProcessor.getOrderedFeedbackSessionLogs(courseId, email, startTime, endTime, feedbackSessionName); Map studentsMap = new HashMap<>(); Map sessionsMap = new HashMap<>(); List feedbackSessions = logic.getFeedbackSessionsForCourse(courseId); @@ -215,4 +229,14 @@ private Map> groupFeedbackSessionLogEntrie } return groupedEntries; } + + private Map> groupFeedbackSessionLogs( + List fsLogEntries) { + Map> groupedEntries = new LinkedHashMap<>(); + for (FeedbackSessionLog fsLogEntry : fsLogEntries) { + String fsName = fsLogEntry.getFeedbackSession().getName(); + groupedEntries.computeIfAbsent(fsName, k -> new ArrayList<>()).add(fsLogEntry); + } + return groupedEntries; + } } diff --git a/src/main/java/teammates/ui/webapi/UpdateFeedbackSessionLogsAction.java b/src/main/java/teammates/ui/webapi/UpdateFeedbackSessionLogsAction.java new file mode 100644 index 00000000000..9324d4615df --- /dev/null +++ b/src/main/java/teammates/ui/webapi/UpdateFeedbackSessionLogsAction.java @@ -0,0 +1,77 @@ +package teammates.ui.webapi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import teammates.common.datatransfer.FeedbackSessionLogEntry; +import teammates.common.datatransfer.attributes.CourseAttributes; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; + +/** + * Process feedback session logs from GCP in the past defined time period and + * store in the database. + */ +public class UpdateFeedbackSessionLogsAction extends AdminOnlyAction { + + static final long COLLECTION_TIME_PERIOD = Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes(); + static final long SPAM_FILTER = Const.STUDENT_ACTIVITY_LOGS_FILTER_WINDOW.toMillis(); + + @Override + public JsonResult execute() { + List filteredLogs = new ArrayList<>(); + + Instant endTime = TimeHelper.getInstantNearestQuarterHourBefore(Instant.now()); + Instant startTime = endTime.minus(COLLECTION_TIME_PERIOD, ChronoUnit.MINUTES); + + List logEntries = logsProcessor.getOrderedFeedbackSessionLogs(null, null, + startTime.toEpochMilli(), endTime.toEpochMilli(), null); + + Map>>> lastSavedTimestamps = new HashMap<>(); + Map isCourseMigratedMap = new HashMap<>(); + for (FeedbackSessionLogEntry logEntry : logEntries) { + + isCourseMigratedMap.computeIfAbsent(logEntry.getCourseId(), k -> { + CourseAttributes course = logic.getCourse(logEntry.getCourseId()); + return course == null || course.isMigrated(); + }); + + if (!isCourseMigratedMap.get(logEntry.getCourseId())) { + continue; + } + + String courseId = logEntry.getCourseId(); + UUID studentId = logEntry.getStudentId(); + UUID fbSessionId = logEntry.getFeedbackSessionId(); + String type = logEntry.getFeedbackSessionLogType(); + Long timestamp = logEntry.getTimestamp(); + + lastSavedTimestamps.computeIfAbsent(studentId, k -> new HashMap<>()); + lastSavedTimestamps.get(studentId).computeIfAbsent(courseId, k -> new HashMap<>()); + lastSavedTimestamps.get(studentId).get(courseId).computeIfAbsent(fbSessionId, k -> new HashMap<>()); + Long lastSaved = lastSavedTimestamps.get(studentId).get(courseId).get(fbSessionId).getOrDefault(type, 0L); + + if (Math.abs(timestamp - lastSaved) > SPAM_FILTER) { + lastSavedTimestamps.get(studentId).get(courseId).get(fbSessionId).put(type, timestamp); + Student student = sqlLogic.getStudentReference(studentId); + FeedbackSession feedbackSession = sqlLogic.getFeedbackSessionReference(fbSessionId); + FeedbackSessionLog fslEntity = new FeedbackSessionLog(student, feedbackSession, + FeedbackSessionLogType.valueOfLabel(type), Instant.ofEpochMilli(timestamp)); + filteredLogs.add(fslEntity); + } + } + + sqlLogic.createFeedbackSessionLogs(filteredLogs); + + return new JsonResult("Successful"); + } +} diff --git a/src/test/java/teammates/common/util/TimeHelperTest.java b/src/test/java/teammates/common/util/TimeHelperTest.java index 3c805252f97..9f9ca7035ab 100644 --- a/src/test/java/teammates/common/util/TimeHelperTest.java +++ b/src/test/java/teammates/common/util/TimeHelperTest.java @@ -146,4 +146,68 @@ public void testGetInstantMonthsOffsetFromNow() { assertEquals(expected, actual); } + @Test + public void getInstantNearestQuarterHourBefore() { + Instant expectedQ1 = Instant.parse("2020-12-31T16:00:00Z"); + Instant actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:00:00Z")); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:09:30Z")); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:14:59Z")); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper + .getInstantNearestQuarterHourBefore(OffsetDateTime.parse("2021-01-01T00:10:00+08:00").toInstant()); + + assertEquals(expectedQ1, actual); + + actual = TimeHelper + .getInstantNearestQuarterHourBefore(OffsetDateTime.parse("2020-12-31T12:09:00-04:00").toInstant()); + + assertEquals(expectedQ1, actual); + + Instant expectedQ2 = Instant.parse("2020-12-31T16:15:00Z"); + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:15:00Z")); + + assertEquals(expectedQ2, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:19:30Z")); + + assertEquals(expectedQ2, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:29:59Z")); + + assertEquals(expectedQ2, actual); + + Instant expectedQ3 = Instant.parse("2020-12-31T16:30:00Z"); + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:30:00Z")); + + assertEquals(expectedQ3, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:39:30Z")); + + assertEquals(expectedQ3, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:44:59Z")); + + assertEquals(expectedQ3, actual); + + Instant expectedQ4 = Instant.parse("2020-12-31T16:45:00Z"); + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:45:00Z")); + + assertEquals(expectedQ4, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:49:30Z")); + + assertEquals(expectedQ4, actual); + + actual = TimeHelper.getInstantNearestQuarterHourBefore(Instant.parse("2020-12-31T16:59:59Z")); + + assertEquals(expectedQ4, actual); + } } diff --git a/src/test/java/teammates/logic/api/MockLogsProcessor.java b/src/test/java/teammates/logic/api/MockLogsProcessor.java index dc8a90ed9fd..60299b538be 100644 --- a/src/test/java/teammates/logic/api/MockLogsProcessor.java +++ b/src/test/java/teammates/logic/api/MockLogsProcessor.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.UUID; import teammates.common.datatransfer.FeedbackSessionLogEntry; import teammates.common.datatransfer.QueryLogsResults; @@ -23,9 +24,19 @@ public class MockLogsProcessor extends LogsProcessor { /** * Simulates insertion of feedback session logs. */ - public void insertFeedbackSessionLog(String studentEmail, String feedbackSessionName, + public void insertFeedbackSessionLog(String courseId, String studentEmail, String feedbackSessionName, String fslType, long timestamp) { - feedbackSessionLogs.add(new FeedbackSessionLogEntry(studentEmail, feedbackSessionName, fslType, timestamp)); + feedbackSessionLogs + .add(new FeedbackSessionLogEntry(courseId, studentEmail, feedbackSessionName, fslType, timestamp)); + } + + /** + * Simulates insertion of feedback session logs. + */ + public void insertFeedbackSessionLog(String courseId, UUID studentId, UUID feedbackSessionId, + String fslType, long timestamp) { + feedbackSessionLogs + .add(new FeedbackSessionLogEntry(courseId, studentId, feedbackSessionId, fslType, timestamp)); } /** @@ -97,13 +108,14 @@ public QueryLogsResults queryLogs(QueryLogsParams queryLogsParams) { } @Override - public void createFeedbackSessionLog(String courseId, String email, String fsName, String fslType) { + public void createFeedbackSessionLog(String courseId, UUID studentId, UUID fsId, String fslType) { // No-op } @Override - public List getFeedbackSessionLogs(String courseId, String email, + public List getOrderedFeedbackSessionLogs(String courseId, String email, long startTime, long endTime, String fsName) { + feedbackSessionLogs.sort((x, y) -> x.compareTo(y)); return feedbackSessionLogs; } diff --git a/src/test/java/teammates/sqlui/webapi/GetFeedbackSessionLogsActionTest.java b/src/test/java/teammates/sqlui/webapi/GetFeedbackSessionLogsActionTest.java new file mode 100644 index 00000000000..73af8c67f3d --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/GetFeedbackSessionLogsActionTest.java @@ -0,0 +1,330 @@ +package teammates.sqlui.webapi; + +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.InstructorPrivileges; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.Const; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Instructor; +import teammates.storage.sqlentity.Student; +import teammates.ui.output.FeedbackSessionLogData; +import teammates.ui.output.FeedbackSessionLogEntryData; +import teammates.ui.output.FeedbackSessionLogsData; +import teammates.ui.webapi.GetFeedbackSessionLogsAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link GetFeedbackSessionLogsAction}. + */ +public class GetFeedbackSessionLogsActionTest extends BaseActionTest { + + private Course course; + + private Student student1; + private Student student2; + + private FeedbackSession fs1; + + private long startTime; + private long endTime; + + private String googleId = "google-id"; + + @Override + String getActionUri() { + return Const.ResourceURIs.SESSION_LOGS; + } + + @Override + String getRequestMethod() { + return GET; + } + + @BeforeMethod + void setUp() { + FeedbackSession fs2; + endTime = Instant.now().toEpochMilli(); + startTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() - 1) * 24 * 60 * 60 * 1000; + + course = getTypicalCourse(); + + student1 = getTypicalStudent(); + student1.setEmail("student1@teammates.tmt"); + student1.setTeam(getTypicalTeam()); + + student2 = getTypicalStudent(); + student2.setEmail("student2@teammates.tmt"); + student2.setTeam(getTypicalTeam()); + + fs1 = getTypicalFeedbackSessionForCourse(course); + fs1.setName("fs1"); + fs1.setCreatedAt(Instant.now()); + + fs2 = getTypicalFeedbackSessionForCourse(course); + fs2.setName("fs2"); + fs2.setCreatedAt(Instant.now()); + + when(mockLogic.getCourse(course.getId())).thenReturn(course); + when(mockLogic.getFeedbackSession(fs1.getId())).thenReturn(fs1); + when(mockLogic.getStudent(student1.getId())).thenReturn(student1); + when(mockLogic.getStudent(student2.getId())).thenReturn(student2); + + List feedbackSessions = new ArrayList<>(); + feedbackSessions.add(fs1); + feedbackSessions.add(fs2); + when(mockLogic.getFeedbackSessionsForCourse(course.getId())).thenReturn(feedbackSessions); + + FeedbackSessionLog student1Session1Log1 = new FeedbackSessionLog(student1, fs1, FeedbackSessionLogType.ACCESS, + Instant.ofEpochMilli(startTime)); + FeedbackSessionLog student1Session2Log1 = new FeedbackSessionLog(student1, fs2, FeedbackSessionLogType.ACCESS, + Instant.ofEpochMilli(startTime + 1000)); + FeedbackSessionLog student1Session2Log2 = new FeedbackSessionLog(student1, fs2, + FeedbackSessionLogType.SUBMISSION, Instant.ofEpochMilli(startTime + 2000)); + FeedbackSessionLog student2Session1Log1 = new FeedbackSessionLog(student2, fs1, FeedbackSessionLogType.ACCESS, + Instant.ofEpochMilli(startTime + 3000)); + FeedbackSessionLog student2Session1Log2 = new FeedbackSessionLog(student2, fs1, + FeedbackSessionLogType.SUBMISSION, Instant.ofEpochMilli(startTime + 4000)); + + List allLogsInCourse = new ArrayList<>(); + allLogsInCourse.add(student1Session1Log1); + allLogsInCourse.add(student1Session2Log1); + allLogsInCourse.add(student1Session2Log2); + allLogsInCourse.add(student2Session1Log1); + allLogsInCourse.add(student2Session1Log2); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), null, null, Instant.ofEpochMilli(startTime), + Instant.ofEpochMilli(endTime))).thenReturn(allLogsInCourse); + + List student1Logs = new ArrayList<>(); + student1Logs.add(student1Session1Log1); + student1Logs.add(student1Session2Log1); + student1Logs.add(student1Session2Log2); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), null, + Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime))).thenReturn(student1Logs); + + List fs1Logs = new ArrayList<>(); + fs1Logs.add(student1Session1Log1); + fs1Logs.add(student2Session1Log1); + fs1Logs.add(student2Session1Log2); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), null, fs1.getId(), + Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime))).thenReturn(fs1Logs); + + List student1Fs1Logs = new ArrayList<>(); + student1Fs1Logs.add(student1Session1Log1); + when(mockLogic.getOrderedFeedbackSessionLogs(course.getId(), student1.getId(), fs1.getId(), + Instant.ofEpochMilli(startTime), Instant.ofEpochMilli(endTime))).thenReturn(student1Fs1Logs); + } + + @Test + protected void testExecute() { + JsonResult actionOutput; + + ______TS("Failure case: not enough parameters"); + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, course.getId()); + + verifyHttpParameterFailure( + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime)); + verifyHttpParameterFailure( + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime)); + + ______TS("Failure case: invalid course id"); + String[] paramsInvalid1 = { + Const.ParamsNames.COURSE_ID, "fake-course-id", + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + verifyEntityNotFound(paramsInvalid1); + + ______TS("Failure case: invalid student id"); + String[] paramsInvalid2 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.STUDENT_SQL_ID, "00000000-0000-0000-0000-000000000000", + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + verifyEntityNotFound(paramsInvalid2); + + ______TS("Failure case: invalid start or end times"); + String[] paramsInvalid3 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, "abc", + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + verifyHttpParameterFailure(paramsInvalid3); + + String[] paramsInvalid4 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, " ", + }; + verifyHttpParameterFailure(paramsInvalid4); + + ______TS("Success case: should group by feedback session"); + String[] paramsSuccessful1 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + + actionOutput = getJsonResult(getAction(paramsSuccessful1)); + + FeedbackSessionLogsData fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + List fsLogs = fslData.getFeedbackSessionLogs(); + + // Course has 2 feedback sessions + assertEquals(fsLogs.size(), 2); + + List fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + List fsLogEntries2 = fsLogs.get(1).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 3); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(1).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(2).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(2).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + assertEquals(fsLogEntries2.size(), 2); + assertEquals(fsLogEntries2.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept optional student id"); + String[] paramsSuccessful2 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful2)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 2); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + fsLogEntries2 = fsLogs.get(1).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 1); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + + assertEquals(fsLogEntries2.size(), 2); + assertEquals(fsLogEntries2.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries2.get(1).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries2.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept optional feedback session"); + String[] paramsSuccessful3 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful3)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 2); + assertEquals(fsLogs.get(1).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 3); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(1).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(1).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + assertEquals(fsLogEntries1.get(2).getStudentData().getEmail(), student2.getEmail()); + assertEquals(fsLogEntries1.get(2).getFeedbackSessionLogType(), FeedbackSessionLogType.SUBMISSION); + + ______TS("Success case: should accept all optional params"); + String[] paramsSuccessful4 = { + Const.ParamsNames.COURSE_ID, course.getId(), + Const.ParamsNames.STUDENT_SQL_ID, student1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_ID, fs1.getId().toString(), + Const.ParamsNames.FEEDBACK_SESSION_LOG_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.FEEDBACK_SESSION_LOG_ENDTIME, String.valueOf(endTime), + }; + actionOutput = getJsonResult(getAction(paramsSuccessful4)); + fslData = (FeedbackSessionLogsData) actionOutput.getOutput(); + fsLogs = fslData.getFeedbackSessionLogs(); + + assertEquals(fsLogs.size(), 2); + assertEquals(fsLogs.get(1).getFeedbackSessionLogEntries().size(), 0); + + fsLogEntries1 = fsLogs.get(0).getFeedbackSessionLogEntries(); + + assertEquals(fsLogEntries1.size(), 1); + assertEquals(fsLogEntries1.get(0).getStudentData().getEmail(), student1.getEmail()); + assertEquals(fsLogEntries1.get(0).getFeedbackSessionLogType(), FeedbackSessionLogType.ACCESS); + + // TODO: if we restrict the range from start to end time, it should be tested + // here as well + } + + @Test + void testSpecificAccessControl_instructorWithInvalidPermission_cannotAccess() { + + Instructor instructor = new Instructor(course, "name", "instructoremail@tm.tmt", + false, "", null, new InstructorPrivileges()); + + loginAsInstructor(googleId); + when(mockLogic.getCourse(course.getId())).thenReturn(course); + when(mockLogic.getInstructorByGoogleId(course.getId(), googleId)).thenReturn(instructor); + + String[] params = { + Const.ParamsNames.COURSE_ID, course.getId(), + }; + + verifyCannotAccess(params); + } + + @Test + void testSpecificAccessControl_instructorWithPermission_canAccess() { + InstructorPrivileges instructorPrivileges = new InstructorPrivileges(); + instructorPrivileges.updatePrivilege(Const.InstructorPermissions.CAN_MODIFY_SESSION, true); + instructorPrivileges.updatePrivilege(Const.InstructorPermissions.CAN_MODIFY_STUDENT, true); + instructorPrivileges.updatePrivilege(Const.InstructorPermissions.CAN_MODIFY_INSTRUCTOR, true); + Instructor instructor = new Instructor(course, "name", "instructoremail@tm.tmt", + false, "", null, instructorPrivileges); + + loginAsInstructor(googleId); + when(mockLogic.getCourse(course.getId())).thenReturn(course); + when(mockLogic.getInstructorByGoogleId(course.getId(), googleId)).thenReturn(instructor); + + String[] params = { + Const.ParamsNames.COURSE_ID, course.getId(), + }; + + verifyCanAccess(params); + } + + @Test + void testSpecificAccessControl_notInstructor_cannotAccess() { + String[] params = { + Const.ParamsNames.COURSE_ID, course.getId(), + }; + loginAsStudent(googleId); + verifyCannotAccess(params); + + logoutUser(); + verifyCannotAccess(params); + } +} diff --git a/src/test/java/teammates/sqlui/webapi/UpdateFeedbackSessionLogsActionTest.java b/src/test/java/teammates/sqlui/webapi/UpdateFeedbackSessionLogsActionTest.java new file mode 100644 index 00000000000..6fcaf98daff --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/UpdateFeedbackSessionLogsActionTest.java @@ -0,0 +1,233 @@ +package teammates.sqlui.webapi; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.FeedbackSessionLogEntry; +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.TimeHelper; +import teammates.storage.sqlentity.Course; +import teammates.storage.sqlentity.FeedbackSession; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.storage.sqlentity.Student; +import teammates.ui.webapi.UpdateFeedbackSessionLogsAction; + +/** + * SUT: {@link UpdateFeedbackSessionLogsAction}. + */ +public class UpdateFeedbackSessionLogsActionTest + extends BaseActionTest { + + static final long COLLECTION_TIME_PERIOD = Const.STUDENT_ACTIVITY_LOGS_UPDATE_INTERVAL.toMinutes(); + static final long SPAM_FILTER = Const.STUDENT_ACTIVITY_LOGS_FILTER_WINDOW.toMillis(); + + Student student1; + Student student2; + + Course course1; + Course course2; + + FeedbackSession session1InCourse1; + FeedbackSession session2InCourse1; + FeedbackSession session1InCourse2; + + Instant endTime; + Instant startTime; + + @Override + protected String getActionUri() { + return Const.CronJobURIs.AUTOMATED_FEEDBACK_SESSION_LOGS_PROCESSING; + } + + @Override + String getRequestMethod() { + return GET; + } + + @BeforeMethod + void setUp() { + endTime = TimeHelper.getInstantNearestQuarterHourBefore(Instant.now()); + startTime = endTime.minus(COLLECTION_TIME_PERIOD, ChronoUnit.MINUTES); + + course1 = getTypicalCourse(); + course1.setId("course1"); + + course2 = getTypicalCourse(); + course2.setId("course2"); + + student1 = getTypicalStudent(); + student1.setEmail("student1@teammates.tmt"); + student1.setId(UUID.randomUUID()); + + student2 = getTypicalStudent(); + student2.setEmail("student2@teammates.tmt"); + student2.setId(UUID.randomUUID()); + + session1InCourse1 = getTypicalFeedbackSessionForCourse(course1); + session1InCourse1.setName("session1"); + session1InCourse1.setId(UUID.randomUUID()); + + session2InCourse1 = getTypicalFeedbackSessionForCourse(course1); + session2InCourse1.setName("session2"); + session2InCourse1.setId(UUID.randomUUID()); + + session1InCourse2 = getTypicalFeedbackSessionForCourse(course2); + session1InCourse2.setName("session1"); + session1InCourse2.setId(UUID.randomUUID()); + + reset(mockLogic); + + when(mockLogic.getStudentReference(student1.getId())).thenReturn(student1); + when(mockLogic.getStudentReference(student2.getId())).thenReturn(student2); + + when(mockLogic.getFeedbackSessionReference(session1InCourse1.getId())).thenReturn(session1InCourse1); + when(mockLogic.getFeedbackSessionReference(session2InCourse1.getId())).thenReturn(session2InCourse1); + when(mockLogic.getFeedbackSessionReference(session1InCourse2.getId())).thenReturn(session1InCourse2); + + mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, "").clear(); + } + + @Test + public void testExecute_noRecentLogs_noLogsCreated() + throws EntityAlreadyExistsException, InvalidParametersException { + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + verify(mockLogic).createFeedbackSessionLogs(argThat(filteredLogs -> filteredLogs.size() == 0)); + } + + @Test + public void testExecute_recentLogsNoSpam_allLogsCreated() + throws EntityAlreadyExistsException, InvalidParametersException { + // Different Types + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.SUBMISSION.getLabel(), startTime.plusSeconds(100).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.VIEW_RESULT.getLabel(), startTime.plusSeconds(100).toEpochMilli()); + + // Different feedback sessions + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session2InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(200).toEpochMilli()); + + // Different Student + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(300).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student2.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(300).toEpochMilli()); + + // Different course + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(400).toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course2.getId(), student1.getId(), session1InCourse2.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusSeconds(400).toEpochMilli()); + + // Gap is larger than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + // method returns all logs regardless of params + List expected = mockLogsProcessor.getOrderedFeedbackSessionLogs("", "", 0, 0, ""); + + verify(mockLogic).createFeedbackSessionLogs(argThat(filteredLogs -> isEqual(expected, filteredLogs))); + } + + @Test + public void testExecute_recentLogsWithSpam_someLogsCreated() + throws EntityAlreadyExistsException, InvalidParametersException { + // Gap is smaller than spam filter + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli()); + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER - 2).toEpochMilli()); + + // Filters multiple logs within one spam window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER - 1).toEpochMilli()); + + // Correctly adds new log after filtering + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli()); + + // Filters out spam in the new window + mockLogsProcessor.insertFeedbackSessionLog(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 2).toEpochMilli()); + + UpdateFeedbackSessionLogsAction action = getAction(); + action.execute(); + + List expected = new ArrayList<>(); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.toEpochMilli())); + expected.add(new FeedbackSessionLogEntry(course1.getId(), student1.getId(), session1InCourse1.getId(), + FeedbackSessionLogType.ACCESS.getLabel(), startTime.plusMillis(SPAM_FILTER + 1).toEpochMilli())); + + verify(mockLogic).createFeedbackSessionLogs(argThat(filteredLogs -> isEqual(expected, filteredLogs))); + } + + @Test + public void testSpecificAccessControl_isAdmin_canAccess() { + loginAsAdmin(); + verifyCanAccess(); + } + + @Test + public void testSpecificAccessControl_isInstructor_cannotAccess() { + loginAsInstructor("user-id"); + verifyCannotAccess(); + } + + @Test + public void testSpecificAccessControl_isStudent_cannotAccess() { + loginAsStudent("user-id"); + verifyCannotAccess(); + } + + @Test + public void testSpecificAccessControl_loggedOut_cannotAccess() { + logoutUser(); + verifyCannotAccess(); + } + + private Boolean isEqual(List expected, List actual) { + + assertEquals(expected.size(), actual.size()); + + for (int i = 0; i < expected.size(); i++) { + FeedbackSessionLogEntry expectedEntry = expected.get(i); + FeedbackSessionLog actualLog = actual.get(i); + + assertEquals(expectedEntry.getStudentId(), actualLog.getStudent().getId()); + + assertEquals(expectedEntry.getFeedbackSessionId(), actualLog.getFeedbackSession().getId()); + + assertEquals(expectedEntry.getFeedbackSessionLogType(), actualLog.getFeedbackSessionLogType().getLabel()); + + assertEquals(expectedEntry.getTimestamp(), actualLog.getTimestamp().toEpochMilli()); + } + + return true; + } +} diff --git a/src/test/java/teammates/storage/sqlapi/FeedbackSessionLogsDbTest.java b/src/test/java/teammates/storage/sqlapi/FeedbackSessionLogsDbTest.java new file mode 100644 index 00000000000..ee6bf29b1ff --- /dev/null +++ b/src/test/java/teammates/storage/sqlapi/FeedbackSessionLogsDbTest.java @@ -0,0 +1,46 @@ +package teammates.storage.sqlapi; + +import static org.mockito.Mockito.mockStatic; + +import java.time.Instant; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.logs.FeedbackSessionLogType; +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.FeedbackSessionLog; +import teammates.test.BaseTestCase; + +/** + * SUT: {@code FeedbackSessionLogsDb}. + */ +public class FeedbackSessionLogsDbTest extends BaseTestCase { + + private FeedbackSessionLogsDb feedbackSessionLogsDb = FeedbackSessionLogsDb.inst(); + + private MockedStatic mockHibernateUtil; + + @BeforeMethod + public void setUpMethod() { + mockHibernateUtil = mockStatic(HibernateUtil.class); + } + + @AfterMethod + public void teardownMethod() { + mockHibernateUtil.close(); + } + + @Test + public void testCreateFeedbackSessionLog_success() { + + FeedbackSessionLog logToAdd = new FeedbackSessionLog(getTypicalStudent(), + getTypicalFeedbackSessionForCourse(getTypicalCourse()), FeedbackSessionLogType.ACCESS, + Instant.parse("2011-01-01T00:00:00Z")); + feedbackSessionLogsDb.createFeedbackSessionLog(logToAdd); + + mockHibernateUtil.verify(() -> HibernateUtil.persist(logToAdd)); + } +} diff --git a/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java b/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java index dd5974c4243..dad8b7ad1a4 100644 --- a/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java +++ b/src/test/java/teammates/ui/webapi/CreateFeedbackSessionLogActionTest.java @@ -7,6 +7,7 @@ import teammates.common.datatransfer.attributes.StudentAttributes; import teammates.common.datatransfer.logs.FeedbackSessionLogType; import teammates.common.util.Const; +import teammates.ui.output.MessageOutput; /** * SUT: {@link CreateFeedbackSessionLogAction}. @@ -61,7 +62,9 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.ACCESS.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), }; - getJsonResult(getAction(paramsSuccessfulAccess)); + JsonResult response = getJsonResult(getAction(paramsSuccessfulAccess)); + MessageOutput output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); ______TS("Success case: typical submission"); String[] paramsSuccessfulSubmission = { @@ -70,24 +73,20 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student2.getEmail(), }; - getJsonResult(getAction(paramsSuccessfulSubmission)); + response = getJsonResult(getAction(paramsSuccessfulSubmission)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); ______TS("Success case: should create even for invalid parameters"); - String[] paramsNonExistentCourseId = { - Const.ParamsNames.COURSE_ID, "non-existent-course-id", - Const.ParamsNames.FEEDBACK_SESSION_NAME, fsa1.getFeedbackSessionName(), - Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), - Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), - }; - getJsonResult(getAction(paramsNonExistentCourseId)); - String[] paramsNonExistentFsName = { Const.ParamsNames.COURSE_ID, courseId1, Const.ParamsNames.FEEDBACK_SESSION_NAME, "non-existent-feedback-session-name", Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student1.getEmail(), }; - getJsonResult(getAction(paramsNonExistentFsName)); + response = getJsonResult(getAction(paramsNonExistentFsName)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); String[] paramsNonExistentStudentEmail = { Const.ParamsNames.COURSE_ID, courseId1, @@ -95,7 +94,9 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, "non-existent-student@email.com", }; - getJsonResult(getAction(paramsNonExistentStudentEmail)); + response = getJsonResult(getAction(paramsNonExistentStudentEmail)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); ______TS("Success case: should create even when student cannot access feedback session in course"); String[] paramsWithoutAccess = { @@ -104,7 +105,9 @@ protected void testExecute() { Const.ParamsNames.FEEDBACK_SESSION_LOG_TYPE, FeedbackSessionLogType.SUBMISSION.getLabel(), Const.ParamsNames.STUDENT_EMAIL, student3.getEmail(), }; - getJsonResult(getAction(paramsWithoutAccess)); + response = getJsonResult(getAction(paramsWithoutAccess)); + output = (MessageOutput) response.getOutput(); + assertEquals("Successful", output.getMessage()); } @Test diff --git a/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java b/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java index fbcd4a0765c..4493e828de6 100644 --- a/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java +++ b/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java @@ -143,7 +143,8 @@ protected void testExecute() { GetDeadlineExtensionAction.class, SendLoginEmailAction.class, PutSqlDataBundleAction.class, - DeleteSqlDataBundleAction.class + DeleteSqlDataBundleAction.class, + UpdateFeedbackSessionLogsAction.class ); List expectedActionClassesNames = expectedActionClasses.stream() .map(Class::getSimpleName) diff --git a/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java b/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java index c668a9394f8..41b1b0421bc 100644 --- a/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java +++ b/src/test/java/teammates/ui/webapi/GetFeedbackSessionLogsActionTest.java @@ -48,15 +48,15 @@ protected void testExecute() { long startTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() - 1) * 24 * 60 * 60 * 1000; long invalidStartTime = endTime - (Const.LOGS_RETENTION_PERIOD.toDays() + 1) * 24 * 60 * 60 * 1000; - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa1Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student1Email, fsa1Name, FeedbackSessionLogType.ACCESS.getLabel(), startTime); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student1Email, fsa2Name, FeedbackSessionLogType.ACCESS.getLabel(), startTime + 1000); - mockLogsProcessor.insertFeedbackSessionLog(student1Email, fsa2Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student1Email, fsa2Name, FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 2000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student2Email, fsa1Name, FeedbackSessionLogType.ACCESS.getLabel(), startTime + 3000); - mockLogsProcessor.insertFeedbackSessionLog(student2Email, fsa1Name, + mockLogsProcessor.insertFeedbackSessionLog(courseId, student2Email, fsa1Name, FeedbackSessionLogType.SUBMISSION.getLabel(), startTime + 4000); ______TS("Failure case: not enough parameters"); diff --git a/src/web/app/pages-instructor/instructor-courses-page/instructor-courses-page.component.html b/src/web/app/pages-instructor/instructor-courses-page/instructor-courses-page.component.html index b8d08573d05..5926e1db649 100644 --- a/src/web/app/pages-instructor/instructor-courses-page/instructor-courses-page.component.html +++ b/src/web/app/pages-instructor/instructor-courses-page/instructor-courses-page.component.html @@ -166,9 +166,10 @@

Active courses

placement="left" container="body"> Archive - - - + + View Logs +