From 4edfb042fe434bf1d2226f528476caa69dc6e476 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Sat, 14 Sep 2024 14:13:46 -0400 Subject: [PATCH 1/2] Validate session dates --- .../src/components/ControlledDatePicker.js | 24 ++++++-- .../__tests__/ControlledDatePicker.js | 57 ++++++++++++++++++- frontend/src/pages/SessionForm/index.js | 4 ++ .../pages/SessionForm/pages/sessionSummary.js | 20 ++++++- 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/ControlledDatePicker.js b/frontend/src/components/ControlledDatePicker.js index bb0eedcd87..7615f255f3 100644 --- a/frontend/src/components/ControlledDatePicker.js +++ b/frontend/src/components/ControlledDatePicker.js @@ -21,6 +21,7 @@ export default function ControlledDatePicker({ isStartDate, inputId, endDate, + customValidationMessages, required, }) { /** @@ -54,20 +55,25 @@ export default function ControlledDatePicker({ const formattedValue = value ? moment(value, DATE_DISPLAY_FORMAT).format(DATEPICKER_VALUE_FORMAT) : ''; + const { + beforeMessage, + afterMessage, + invalidMessage, + } = customValidationMessages; + // this is our custom validation function we pass to the hook form controller function validate(v) { const newValue = moment(v, DATE_DISPLAY_FORMAT); - if (!newValue.isValid()) { - return 'Enter valid date'; + return invalidMessage || 'Enter valid date'; } if (newValue.isBefore(min.moment)) { - return `Please enter a date after ${min.display}`; + return afterMessage || `Please enter a date after ${min.display}`; } if (newValue.isAfter(max.moment)) { - return `Please enter a date before ${max.display}`; + return beforeMessage || `Please enter a date before ${max.display}`; } return true; @@ -135,6 +141,11 @@ ControlledDatePicker.propTypes = { inputId: PropTypes.string.isRequired, endDate: PropTypes.string, required: PropTypes.bool, + customValidationMessages: PropTypes.shape({ + beforeMessage: PropTypes.string, + afterMessage: PropTypes.string, + invalidMessage: PropTypes.string, + }), }; ControlledDatePicker.defaultProps = { @@ -145,4 +156,9 @@ ControlledDatePicker.defaultProps = { setEndDate: () => {}, required: true, value: '', + customValidationMessages: { + beforeMessage: '', + afterMessage: '', + invalidMessage: '', + }, }; diff --git a/frontend/src/components/__tests__/ControlledDatePicker.js b/frontend/src/components/__tests__/ControlledDatePicker.js index 36dec43be1..f17e073350 100644 --- a/frontend/src/components/__tests__/ControlledDatePicker.js +++ b/frontend/src/components/__tests__/ControlledDatePicker.js @@ -2,17 +2,24 @@ import '@testing-library/jest-dom'; import React from 'react'; import moment from 'moment'; -import { render, screen, act } from '@testing-library/react'; +import { + render, screen, act, fireEvent, +} from '@testing-library/react'; import { useForm } from 'react-hook-form'; import userEvent from '@testing-library/user-event'; import { Grid } from '@trussworks/react-uswds'; import { DATE_DISPLAY_FORMAT } from '../../Constants'; - import ControlledDatePicker from '../ControlledDatePicker'; +const defaultValidation = { + beforeMessage: '', + afterMessage: '', + invalidMessage: '', +}; + describe('Controlled Date Picker', () => { // eslint-disable-next-line react/prop-types - const TestDatePicker = ({ setEndDate }) => { + const TestDatePicker = ({ setEndDate, customValidationMessages = defaultValidation }) => { const { control, errors, handleSubmit, watch, } = useForm({ @@ -34,9 +41,11 @@ describe('Controlled Date Picker', () => { Start date { minDate={startDate} key="endDateKey" inputId="endDate" + customValidationMessages={customValidationMessages} /> @@ -89,6 +99,47 @@ describe('Controlled Date Picker', () => { expect(setEndDate).toHaveBeenCalled(); }); + it('displays custom validation messages', async () => { + const setEndDate = jest.fn(); + render(); + + const ed = await screen.findByRole('textbox', { name: /end date/i }); + const sd = await screen.findByRole('textbox', { name: /start date/i }); + + act(() => { + userEvent.type(ed, '12/31/2020'); + userEvent.type(sd, '01/03/2021'); + }); + expect(await screen.findByText('Invalid message')).toBeVisible(); + act(() => { + userEvent.clear(sd); + userEvent.clear(ed); + }); + userEvent.type(ed, '12/31/2020'); + userEvent.type(sd, '08/31/2020'); + + fireEvent.blur(sd); + + expect(await screen.findByText('After message')).toBeVisible(); + + act(() => { + userEvent.clear(sd); + userEvent.clear(ed); + }); + userEvent.type(ed, '12/31/2020'); + userEvent.type(sd, '01/01/2021'); + + fireEvent.blur(ed); + expect(await screen.findByText('Before message')).toBeVisible(); + }); + it('can set future start date and adjust end date', async () => { const setEndDate = jest.fn(); render(); diff --git a/frontend/src/pages/SessionForm/index.js b/frontend/src/pages/SessionForm/index.js index 2011f0f38a..e0d2042178 100644 --- a/frontend/src/pages/SessionForm/index.js +++ b/frontend/src/pages/SessionForm/index.js @@ -248,6 +248,7 @@ export default function SessionForm({ match }) { } try { const session = await getSessionBySessionId(sessionId); + // eslint-disable-next-line max-len const isPocFromSession = (session.event.pocIds || []).includes(user.id) && !isAdminUser; resetFormData(hookForm.reset, session, isPocFromSession, isAdminUser); reportId.current = session.id; @@ -440,6 +441,8 @@ export default function SessionForm({ match }) { } }; + const { event } = formData; + return (
{ error @@ -504,6 +507,7 @@ export default function SessionForm({ match }) { status: formData.status, pages: applicationPages, isAdminUser, + event, }} formData={formData} pages={applicationPages} diff --git a/frontend/src/pages/SessionForm/pages/sessionSummary.js b/frontend/src/pages/SessionForm/pages/sessionSummary.js index f03180db61..738090dd97 100644 --- a/frontend/src/pages/SessionForm/pages/sessionSummary.js +++ b/frontend/src/pages/SessionForm/pages/sessionSummary.js @@ -53,7 +53,7 @@ const DEFAULT_RESOURCE = { value: '', }; -const SessionSummary = ({ datePickerKey }) => { +const SessionSummary = ({ datePickerKey, event }) => { const { setIsAppLoading, setAppLoadingText } = useContext(AppLoadingContext); const { @@ -70,6 +70,8 @@ const SessionSummary = ({ datePickerKey }) => { const { id } = data; + const { startDate: eventStartDate } = (event || { data: { startDate: null } }).data; + const startDate = watch('startDate'); const endDate = watch('endDate'); const courses = watch('courses'); @@ -274,6 +276,10 @@ const SessionSummary = ({ datePickerKey }) => { isStartDate inputId="startDate" endDate={endDate} + minDate={eventStartDate} + customValidationMessages={{ + afterMessage: 'Date selected can\'t be before event start date.', + }} /> @@ -628,6 +634,15 @@ const SessionSummary = ({ datePickerKey }) => { SessionSummary.propTypes = { datePickerKey: PropTypes.string.isRequired, + event: PropTypes.shape({ + data: { + endDate: PropTypes.string, + }, + }), +}; + +SessionSummary.defaultProps = { + event: null, }; const fields = [...Object.keys(sessionSummaryFields), 'endDate', 'startDate']; @@ -667,7 +682,7 @@ export default { Alert, ) => (
- +
{ @@ -684,7 +699,6 @@ export default { && additionalData.status !== TRAINING_REPORT_STATUSES.COMPLETE && ( ) - }
From ed0de9389de042a8714aa587f78377ff63c8366f Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Sat, 14 Sep 2024 14:15:04 -0400 Subject: [PATCH 2/2] Collaborators cannot edit training events --- frontend/src/pages/TrainingReports/components/EventCard.js | 2 +- .../TrainingReports/components/__tests__/EventCards.js | 4 ++-- src/policies/event.js | 2 +- src/policies/event.test.js | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/TrainingReports/components/EventCard.js b/frontend/src/pages/TrainingReports/components/EventCard.js index 5a581e8aa1..13da7fc91b 100644 --- a/frontend/src/pages/TrainingReports/components/EventCard.js +++ b/frontend/src/pages/TrainingReports/components/EventCard.js @@ -49,7 +49,7 @@ function EventCard({ const isComplete = data.status === TRAINING_REPORT_STATUSES.COMPLETE; const isNotCompleteOrSuspended = !isComplete && !isSuspended; - const canEditEvent = ((isOwnerOrCollaborator && !eventSubmitted && isNotCompleteOrSuspended) + const canEditEvent = ((isOwner && !eventSubmitted && isNotCompleteOrSuspended) || (hasAdminRights && isNotCompleteOrSuspended)); const canCreateSession = isNotCompleteOrSuspended && isOwnerOrCollaborator; const canDeleteEvent = hasAdminRights && (data.status === TRAINING_REPORT_STATUSES.NOT_STARTED diff --git a/frontend/src/pages/TrainingReports/components/__tests__/EventCards.js b/frontend/src/pages/TrainingReports/components/__tests__/EventCards.js index 59e43aabe8..248450d05e 100644 --- a/frontend/src/pages/TrainingReports/components/__tests__/EventCards.js +++ b/frontend/src/pages/TrainingReports/components/__tests__/EventCards.js @@ -270,7 +270,7 @@ describe('EventCards', () => { button.click(button); }); - it('collaborators cannot create training', () => { + it('collaborators cannot edit training', () => { const collaboratorEvents = [{ id: 1, ownerId: 3, @@ -314,7 +314,7 @@ describe('EventCards', () => { const button = screen.getByRole('button', { name: /actions for event TR-R01-1234/i }); button.click(button); expect(screen.queryByText(/create session/i)).toBeInTheDocument(); - expect(screen.queryByText(/edit event/i)).toBeInTheDocument(); + expect(screen.queryByText(/edit event/i)).not.toBeInTheDocument(); expect(screen.queryByText(/view event/i)).toBeInTheDocument(); button.click(button); }); diff --git a/src/policies/event.js b/src/policies/event.js index e53643acb4..10df396720 100644 --- a/src/policies/event.js +++ b/src/policies/event.js @@ -140,7 +140,7 @@ export default class EventReport { // some handy & fun aliases canEditEvent() { - return this.isAdmin() || this.isAuthor() || this.isCollaborator(); + return this.isAdmin() || this.isAuthor(); } canCreateSession() { diff --git a/src/policies/event.test.js b/src/policies/event.test.js index 2a12379e73..333c6d2e74 100644 --- a/src/policies/event.test.js +++ b/src/policies/event.test.js @@ -171,16 +171,16 @@ describe('Event Report policies', () => { expect(policy.canEditEvent()).toBe(true); }); - it('is true if the user is a collaborator', () => { + it('is false if the user is a collaborator', () => { const eventRegion1 = createEvent({ ownerId: authorRegion1, collaboratorIds: [authorRegion1Collaborator.id], }); const policy = new EventReport(authorRegion1Collaborator, eventRegion1); - expect(policy.canEditEvent()).toBe(true); + expect(policy.canEditEvent()).toBe(false); }); - it('is true if the user is a poc', () => { + it('is false if the user is a poc', () => { const eventRegion1 = createEvent({ ownerId: authorRegion1, pocIds: [authorRegion1Collaborator.id],