diff --git a/src/main/java/teammates/common/util/FieldValidator.java b/src/main/java/teammates/common/util/FieldValidator.java index 669637729c0..72c79b1eef1 100644 --- a/src/main/java/teammates/common/util/FieldValidator.java +++ b/src/main/java/teammates/common/util/FieldValidator.java @@ -645,7 +645,7 @@ static String getValidityInfoForSizeCappedPossiblyEmptyString(String fieldName, * Checks if the {@code startTime} is valid to be used as a session start time. * Returns an empty string if it is valid, or an error message otherwise. * - *

The {@code startTime} is valid if it is after 2 hours before now, before 90 days from now + *

The {@code startTime} is valid if it is after 2 hours before now, before 12 months from now * and at exact hour mark. */ public static String getInvalidityInfoForNewStartTime(Instant startTime, String timeZone) { @@ -655,18 +655,20 @@ public static String getInvalidityInfoForNewStartTime(Instant startTime, String "2 hours before now", SESSION_START_TIME_FIELD_NAME, (firstTime, secondTime) -> firstTime.isBefore(secondTime) || firstTime.equals(secondTime), "The %s for this %s cannot be earlier than %s."); + if (!earlierThanThreeHoursBeforeNowError.isEmpty()) { return earlierThanThreeHoursBeforeNowError; } - Instant ninetyDaysFromNow = TimeHelper.getInstantDaysOffsetFromNow(90); - String laterThanNinetyDaysFromNowError = getInvalidityInfoForFirstTimeComparedToSecondTime( - ninetyDaysFromNow, startTime, SESSION_NAME, - "90 days from now", SESSION_START_TIME_FIELD_NAME, + Instant twelveMonthsFromNow = TimeHelper.getInstantMonthsOffsetFromNow(12, timeZone); + String laterThanTwelveMonthsFromNowError = getInvalidityInfoForFirstTimeComparedToSecondTime( + twelveMonthsFromNow, startTime, SESSION_NAME, + "12 months from now", SESSION_START_TIME_FIELD_NAME, (firstTime, secondTime) -> firstTime.isAfter(secondTime) || firstTime.equals(secondTime), "The %s for this %s cannot be later than %s."); - if (!laterThanNinetyDaysFromNowError.isEmpty()) { - return laterThanNinetyDaysFromNowError; + + if (!laterThanTwelveMonthsFromNowError.isEmpty()) { + return laterThanTwelveMonthsFromNowError; } String notExactHourError = getInvalidityInfoForExactHourTime(startTime, timeZone, "start time"); @@ -681,7 +683,7 @@ public static String getInvalidityInfoForNewStartTime(Instant startTime, String * Checks if the {@code endTime} is valid to be used as a session end time. * Returns an empty string if it is valid, or an error message otherwise. * - *

The {@code endTime} is valid if it is after 1 hour before now, before 180 days from now + *

The {@code endTime} is valid if it is after 1 hour before now, before 12 months from now * and at exact hour mark. */ public static String getInvalidityInfoForNewEndTime(Instant endTime, String timeZone) { @@ -695,14 +697,14 @@ public static String getInvalidityInfoForNewEndTime(Instant endTime, String time return earlierThanThreeHoursBeforeNowError; } - Instant oneHundredEightyDaysFromNow = TimeHelper.getInstantDaysOffsetFromNow(180); - String laterThanOneHundredEightyDaysError = getInvalidityInfoForFirstTimeComparedToSecondTime( - oneHundredEightyDaysFromNow, endTime, SESSION_NAME, - "180 days from now", SESSION_END_TIME_FIELD_NAME, + Instant twelveMonthsFromNow = TimeHelper.getInstantMonthsOffsetFromNow(12, timeZone); + String laterThanTwelveMonthsError = getInvalidityInfoForFirstTimeComparedToSecondTime( + twelveMonthsFromNow, endTime, SESSION_NAME, + "12 months from now", SESSION_END_TIME_FIELD_NAME, (firstTime, secondTime) -> firstTime.isAfter(secondTime) || firstTime.equals(secondTime), "The %s for this %s cannot be later than %s."); - if (!laterThanOneHundredEightyDaysError.isEmpty()) { - return laterThanOneHundredEightyDaysError; + if (!laterThanTwelveMonthsError.isEmpty()) { + return laterThanTwelveMonthsError; } String notExactHourError = getInvalidityInfoForExactHourTime(endTime, timeZone, "end time"); diff --git a/src/main/java/teammates/common/util/TimeHelper.java b/src/main/java/teammates/common/util/TimeHelper.java index a6d3d209033..8122ff6f39a 100644 --- a/src/main/java/teammates/common/util/TimeHelper.java +++ b/src/main/java/teammates/common/util/TimeHelper.java @@ -39,6 +39,20 @@ public static Instant getInstantDaysOffsetFromNow(long offsetInDays) { return Instant.now().plus(Duration.ofDays(offsetInDays)); } + /** + * Returns an Instant that is offset by a number of months from now. + * + * @param offsetInMonths integer number of months to offset by + * @param timeZone string representing the time zone to compute local datetime + * @return an Instant offset by {@code offsetInMonths} days + */ + public static Instant getInstantMonthsOffsetFromNow(long offsetInMonths, String timeZone) { + Instant now = Instant.now(); + ZonedDateTime zdt = now.atZone(ZoneId.of(timeZone)); + ZonedDateTime offsetZdt = zdt.plusMonths(offsetInMonths); + return offsetZdt.toInstant(); + } + /** * Returns an Instant that is offset by a number of days before now. * diff --git a/src/test/java/teammates/common/util/FieldValidatorTest.java b/src/test/java/teammates/common/util/FieldValidatorTest.java index c3c7746c043..e5b3d9748b5 100644 --- a/src/test/java/teammates/common/util/FieldValidatorTest.java +++ b/src/test/java/teammates/common/util/FieldValidatorTest.java @@ -536,11 +536,11 @@ public void testGetInvalidityInfoForNewStartTime_invalid_returnErrorString() { assertEquals("The start time for this feedback session cannot be earlier than 2 hours before now.", FieldValidator.getInvalidityInfoForNewStartTime(threeHoursBeforeNowRounded, Const.DEFAULT_TIME_ZONE)); - Instant ninetyOneDaysFromNowRounded = TimeHelperExtension - .getInstantDaysOffsetFromNow(91) + Instant thirteenMonthsFromNow = TimeHelperExtension + .getInstantMonthsOffsetFromNow(13, Const.DEFAULT_TIME_ZONE) .truncatedTo(ChronoUnit.HOURS); - assertEquals("The start time for this feedback session cannot be later than 90 days from now.", - FieldValidator.getInvalidityInfoForNewStartTime(ninetyOneDaysFromNowRounded, Const.DEFAULT_TIME_ZONE)); + assertEquals("The start time for this feedback session cannot be later than 12 months from now.", + FieldValidator.getInvalidityInfoForNewStartTime(thirteenMonthsFromNow, Const.DEFAULT_TIME_ZONE)); Instant notAtHourMark = TimeHelperExtension .getInstantHoursOffsetFromNow(1) @@ -571,11 +571,11 @@ public void testGetInvalidityInfoForNewEndTime_invalid_returnErrorString() { assertEquals("The end time for this feedback session cannot be earlier than 1 hour before now.", FieldValidator.getInvalidityInfoForNewEndTime(twoHoursBeforeNowRounded, Const.DEFAULT_TIME_ZONE)); - Instant oneHundredAndEightyOneDaysFromNowRounded = TimeHelperExtension - .getInstantDaysOffsetFromNow(181) + Instant thirteenMonthsFromNow = TimeHelperExtension + .getInstantMonthsOffsetFromNow(13, Const.DEFAULT_TIME_ZONE) .truncatedTo(ChronoUnit.HOURS); - assertEquals("The end time for this feedback session cannot be later than 180 days from now.", - FieldValidator.getInvalidityInfoForNewEndTime(oneHundredAndEightyOneDaysFromNowRounded, + assertEquals("The end time for this feedback session cannot be later than 12 months from now.", + FieldValidator.getInvalidityInfoForNewEndTime(thirteenMonthsFromNow, Const.DEFAULT_TIME_ZONE)); Instant notAtHourMark = TimeHelperExtension diff --git a/src/test/java/teammates/common/util/TimeHelperExtension.java b/src/test/java/teammates/common/util/TimeHelperExtension.java index 7df2f9d4943..8a1397e6e6d 100644 --- a/src/test/java/teammates/common/util/TimeHelperExtension.java +++ b/src/test/java/teammates/common/util/TimeHelperExtension.java @@ -42,6 +42,20 @@ public static Instant getInstantDaysOffsetFromNow(long offsetInDays) { return Instant.now().plus(Duration.ofDays(offsetInDays)); } + /** + * Returns an Instant that is offset by a number of months from now. + * + * @param offsetInMonths integer number of months to offset by + * @param timeZone string representing the time zone to compute local datetime + * @return an Instant offset by {@code offsetInMonths} days + */ + public static Instant getInstantMonthsOffsetFromNow(long offsetInMonths, String timeZone) { + Instant now = Instant.now(); + ZonedDateTime zdt = now.atZone(ZoneId.of(timeZone)); + ZonedDateTime offsetZdt = zdt.plusMonths(offsetInMonths); + return offsetZdt.toInstant(); + } + /** * Returns an java.time.Instant object that is offset by a number of days from now truncated to days. * @param offsetInDays number of days offset by (integer). diff --git a/src/test/java/teammates/common/util/TimeHelperTest.java b/src/test/java/teammates/common/util/TimeHelperTest.java index 052d5d5b22d..3c805252f97 100644 --- a/src/test/java/teammates/common/util/TimeHelperTest.java +++ b/src/test/java/teammates/common/util/TimeHelperTest.java @@ -6,6 +6,7 @@ import java.time.Month; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import org.testng.annotations.Test; @@ -130,4 +131,19 @@ public void testGetInstantHoursOffsetFromNow() { assertEquals(expected, actual); } + @Test + public void testGetInstantMonthsOffsetFromNow() { + Instant expected = Instant.now().truncatedTo(ChronoUnit.DAYS); + Instant actual = TimeHelper.getInstantMonthsOffsetFromNow(0, Const.DEFAULT_TIME_ZONE) + .truncatedTo(ChronoUnit.DAYS); + assertEquals(expected, actual); + + Instant now = Instant.now(); + ZonedDateTime zdt = now.atZone(ZoneId.of(Const.DEFAULT_TIME_ZONE)); + ZonedDateTime offsetZdt = zdt.plusMonths(12); + expected = offsetZdt.toInstant().truncatedTo(ChronoUnit.SECONDS); + actual = TimeHelper.getInstantMonthsOffsetFromNow(12, Const.DEFAULT_TIME_ZONE).truncatedTo(ChronoUnit.SECONDS); + assertEquals(expected, actual); + } + } diff --git a/src/web/app/components/session-edit-form/session-edit-form.component.ts b/src/web/app/components/session-edit-form/session-edit-form.component.ts index 5f6ec0fc37b..1a90937b16e 100644 --- a/src/web/app/components/session-edit-form/session-edit-form.component.ts +++ b/src/web/app/components/session-edit-form/session-edit-form.component.ts @@ -175,17 +175,17 @@ export class SessionEditFormComponent { /** * Gets the maximum date for a session to be opened. * - *

The maximum session opening datetime is 90 days from now. + *

The maximum session opening datetime is 12 months from now. */ get maxDateForSubmissionStart(): DateFormat { - const ninetyDaysFromNow = moment().tz(this.model.timeZone).add(90, 'days'); - return this.datetimeService.getDateInstance(ninetyDaysFromNow); + const twelveMonthsFromNow = moment().tz(this.model.timeZone).add(12, 'months'); + return this.datetimeService.getDateInstance(twelveMonthsFromNow); } /** * Gets the maximum time for a session to be opened. * - *

The maximum session opening datetime is 90 days from now. + *

The maximum session opening time is 23:59h. */ get maxTimeForSubmissionStart(): TimeFormat { return getLatestTimeFormat(); @@ -232,17 +232,17 @@ export class SessionEditFormComponent { /** * Gets the maximum date for a session to be closed. * - *

The maximum session closing datetime is 180 days from now. + *

The maximum session closing datetime is 12 months from now. */ get maxDateForSubmissionEnd(): DateFormat { - const oneHundredAndEightyDaysFromNow = moment().tz(this.model.timeZone).add(180, 'days'); - return this.datetimeService.getDateInstance(oneHundredAndEightyDaysFromNow); + const twelveMonthsFromNow = moment().tz(this.model.timeZone).add(12, 'months'); + return this.datetimeService.getDateInstance(twelveMonthsFromNow); } /** * Gets the maximum time for a session to be closed. * - *

The maximum session closing datetime is 180 days from now. + *

The maximum session closing time is 23:59H. */ get maxTimeForSubmissionEnd(): TimeFormat { return getLatestTimeFormat(); diff --git a/src/web/app/pages-instructor/instructor-home-page/__snapshots__/instructor-home-page.component.spec.ts.snap b/src/web/app/pages-instructor/instructor-home-page/__snapshots__/instructor-home-page.component.spec.ts.snap index b24db421923..946b9490ffd 100644 --- a/src/web/app/pages-instructor/instructor-home-page/__snapshots__/instructor-home-page.component.spec.ts.snap +++ b/src/web/app/pages-instructor/instructor-home-page/__snapshots__/instructor-home-page.component.spec.ts.snap @@ -31,6 +31,7 @@ exports[`InstructorHomePageComponent should snap when courses are still loading numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -105,6 +106,7 @@ exports[`InstructorHomePageComponent should snap with default fields 1`] = ` numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -179,6 +181,7 @@ exports[`InstructorHomePageComponent should snap with one course with error load numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -329,6 +332,7 @@ exports[`InstructorHomePageComponent should snap with one course with one feedba numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -846,6 +850,7 @@ exports[`InstructorHomePageComponent should snap with one course with two feedba numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -1509,6 +1514,7 @@ exports[`InstructorHomePageComponent should snap with one course with unexpanded numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -1760,6 +1766,7 @@ exports[`InstructorHomePageComponent should snap with one course with unpopulate numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} @@ -2035,6 +2042,7 @@ exports[`InstructorHomePageComponent should snap with one course without feedbac numberOfSessionsCopied="0" progressBarService={[Function ProgressBarService]} publishUnpublishRetryAttempts={[Function Number]} + sessionEditFormModel={[Function Object]} simpleModalService={[Function SimpleModalService]} statusMessageService={[Function StatusMessageService]} studentService={[Function StudentService]} diff --git a/src/web/app/pages-instructor/instructor-session-base-page.component.ts b/src/web/app/pages-instructor/instructor-session-base-page.component.ts index e1721dd9465..cdcc85ae338 100644 --- a/src/web/app/pages-instructor/instructor-session-base-page.component.ts +++ b/src/web/app/pages-instructor/instructor-session-base-page.component.ts @@ -17,15 +17,19 @@ import { FeedbackQuestion, FeedbackQuestions, FeedbackSession, + FeedbackSessionPublishStatus, FeedbackSessionStats, + FeedbackSessionSubmissionStatus, ResponseVisibleSetting, SessionVisibleSetting, } from '../../types/api-output'; import { Intent } from '../../types/api-request'; +import { getDefaultDateFormat, getLatestTimeFormat } from '../../types/datetime-const'; import { DEFAULT_NUMBER_OF_RETRY_ATTEMPTS } from '../../types/default-retry-attempts'; import { SortBy, SortOrder } from '../../types/sort-properties'; import { CopySessionModalResult } from '../components/copy-session-modal/copy-session-modal-model'; import { ErrorReportComponent } from '../components/error-report/error-report.component'; +import { SessionEditFormModel } from '../components/session-edit-form/session-edit-form-model'; import { CopySessionResult, SessionsTableRowModel } from '../components/sessions-table/sessions-table-model'; import { SimpleModalType } from '../components/simple-modal/simple-modal-type'; import { ErrorMessageOutput } from '../error-message-output'; @@ -43,6 +47,43 @@ export abstract class InstructorSessionBasePageComponent { private publishUnpublishRetryAttempts: number = DEFAULT_NUMBER_OF_RETRY_ATTEMPTS; + sessionEditFormModel: SessionEditFormModel = { + courseId: '', + timeZone: 'UTC', + courseName: '', + feedbackSessionName: '', + instructions: '', + + submissionStartTime: getLatestTimeFormat(), + submissionStartDate: getDefaultDateFormat(), + submissionEndTime: getLatestTimeFormat(), + submissionEndDate: getDefaultDateFormat(), + gracePeriod: 0, + + sessionVisibleSetting: SessionVisibleSetting.AT_OPEN, + customSessionVisibleTime: getLatestTimeFormat(), + customSessionVisibleDate: getDefaultDateFormat(), + + responseVisibleSetting: ResponseVisibleSetting.CUSTOM, + customResponseVisibleTime: getLatestTimeFormat(), + customResponseVisibleDate: getDefaultDateFormat(), + + submissionStatus: FeedbackSessionSubmissionStatus.OPEN, + publishStatus: FeedbackSessionPublishStatus.NOT_PUBLISHED, + + isClosingEmailEnabled: true, + isPublishedEmailEnabled: true, + + templateSessionName: '', + + isSaving: false, + isEditable: false, + isDeleting: false, + isCopying: false, + hasVisibleSettingsPanelExpanded: false, + hasEmailSettingsPanelExpanded: false, + }; + protected constructor(protected instructorService: InstructorService, protected statusMessageService: StatusMessageService, protected navigationService: NavigationService, @@ -470,6 +511,46 @@ export abstract class InstructorSessionBasePageComponent { modal.componentInstance.requestId = resp.error.requestId; modal.componentInstance.errorMessage = resp.error.message; } + + triggerModelChange(data: SessionEditFormModel): void { + const { submissionStartDate, submissionEndDate, submissionStartTime, submissionEndTime } = data; + + const startDate = new Date(submissionStartDate.year, submissionStartDate.month, submissionStartDate.day); + const endDate = new Date(submissionEndDate.year, submissionEndDate.month, submissionEndDate.day); + + if (startDate > endDate) { + this.sessionEditFormModel = { + ...data, + submissionEndDate: submissionStartDate, + submissionEndTime: + submissionStartTime.hour > submissionEndTime.hour || ( + submissionStartTime.hour === submissionEndTime.hour + && submissionStartTime.minute > submissionEndTime.minute + ) + ? submissionStartTime + : submissionEndTime, + }; + } else if (startDate.toISOString() === endDate.toISOString() && submissionStartTime.hour > submissionEndTime.hour) { + this.sessionEditFormModel = { + ...data, + submissionEndDate: submissionStartDate, + submissionEndTime: { + ...submissionStartTime, + hour: submissionStartTime.hour, + }, + }; + } else if (startDate.toISOString() === endDate.toISOString() && submissionStartTime.hour === submissionEndTime.hour + && submissionStartTime.minute > submissionEndTime.minute) { + this.sessionEditFormModel = { + ...data, + submissionEndDate: submissionStartDate, + submissionEndTime: { + ...submissionStartTime, + minute: submissionStartTime.minute, + }, + }; + } + } } interface SessionTimestampData { diff --git a/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.html b/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.html index bc01c07d24b..9dfb067f970 100644 --- a/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.html +++ b/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.html @@ -2,11 +2,11 @@ (retryEvent)="loadFeedbackSession()"> -

diff --git a/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.ts b/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.ts index 10423cacd5d..921fc82419d 100644 --- a/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.ts +++ b/src/web/app/pages-instructor/instructor-session-edit-page/instructor-session-edit-page.component.ts @@ -32,7 +32,6 @@ import { FeedbackSession, FeedbackSessionPublishStatus, FeedbackSessions, - FeedbackSessionSubmissionStatus, FeedbackTextQuestionDetails, FeedbackVisibilityType, HasResponses, Instructor, @@ -98,44 +97,6 @@ export class InstructorSessionEditPageComponent extends InstructorSessionBasePag isEditingMode: boolean = false; courseName: string = ''; - - // models - sessionEditFormModel: SessionEditFormModel = { - courseId: '', - timeZone: 'UTC', - courseName: '', - feedbackSessionName: '', - instructions: '', - - submissionStartTime: getLatestTimeFormat(), - submissionStartDate: getDefaultDateFormat(), - submissionEndTime: getLatestTimeFormat(), - submissionEndDate: getDefaultDateFormat(), - gracePeriod: 0, - - sessionVisibleSetting: SessionVisibleSetting.AT_OPEN, - customSessionVisibleTime: getLatestTimeFormat(), - customSessionVisibleDate: getDefaultDateFormat(), - - responseVisibleSetting: ResponseVisibleSetting.CUSTOM, - customResponseVisibleTime: getLatestTimeFormat(), - customResponseVisibleDate: getDefaultDateFormat(), - - submissionStatus: FeedbackSessionSubmissionStatus.OPEN, - publishStatus: FeedbackSessionPublishStatus.NOT_PUBLISHED, - - isClosingEmailEnabled: true, - isPublishedEmailEnabled: true, - - templateSessionName: '', - - isSaving: false, - isEditable: false, - isDeleting: false, - isCopying: false, - hasVisibleSettingsPanelExpanded: false, - hasEmailSettingsPanelExpanded: false, - }; studentDeadlines: Record = {}; instructorDeadlines: Record = {}; diff --git a/src/web/app/pages-instructor/instructor-sessions-page/instructor-sessions-page.component.html b/src/web/app/pages-instructor/instructor-sessions-page/instructor-sessions-page.component.html index 326f5798ca8..13506fe75c2 100644 --- a/src/web/app/pages-instructor/instructor-sessions-page/instructor-sessions-page.component.html +++ b/src/web/app/pages-instructor/instructor-sessions-page/instructor-sessions-page.component.html @@ -13,7 +13,7 @@
-