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()">