From 0f483dc4e148c8a6a3e2dac4bf97c6f1a0eb9b69 Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Tue, 12 Dec 2023 14:28:23 -0500 Subject: [PATCH] feat: add escalation email field for LTI-based proctoring providers (#736) This commit adds an escalation email field for LTI-based proctoring providers to the Proctoring modal on the Pages & Resources page. This field behaves identically to the Proctortrack escalation email. --- src/data/services/ExamsApiService.js | 4 +- .../proctoring/Settings.jsx | 128 ++++--- .../proctoring/Settings.test.jsx | 329 ++++++++++-------- .../proctoring/messages.js | 6 +- 4 files changed, 256 insertions(+), 211 deletions(-) diff --git a/src/data/services/ExamsApiService.js b/src/data/services/ExamsApiService.js index 10a6a7526..aa438444d 100644 --- a/src/data/services/ExamsApiService.js +++ b/src/data/services/ExamsApiService.js @@ -1,5 +1,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getConfig } from '@edx/frontend-platform'; +import { convertObjectToSnakeCase } from '../../utils'; class ExamsApiService { static isAvailable() { @@ -26,8 +27,9 @@ class ExamsApiService { } static saveCourseExamConfiguration(courseId, dataToSave) { + const snakecaseDataToSave = convertObjectToSnakeCase(dataToSave, true); const apiClient = getAuthenticatedHttpClient(); - return apiClient.patch(this.getExamConfigurationUrl(courseId), dataToSave); + return apiClient.patch(this.getExamConfigurationUrl(courseId), snakecaseDataToSave); } } diff --git a/src/pages-and-resources/proctoring/Settings.jsx b/src/pages-and-resources/proctoring/Settings.jsx index ecd38769d..d224b066d 100644 --- a/src/pages-and-resources/proctoring/Settings.jsx +++ b/src/pages-and-resources/proctoring/Settings.jsx @@ -28,7 +28,7 @@ const ProctoringSettings = ({ intl, onClose }) => { const initialFormValues = { enableProctoredExams: false, proctoringProvider: false, - proctortrackEscalationEmail: '', + escalationEmail: '', allowOptingOut: false, createZendeskTickets: false, }; @@ -44,7 +44,7 @@ const ProctoringSettings = ({ intl, onClose }) => { const [saveSuccess, setSaveSuccess] = useState(false); const [saveError, setSaveError] = useState(false); const [submissionInProgress, setSubmissionInProgress] = useState(false); - const [showProctortrackEscalationEmail, setShowProctortrackEscalationEmail] = useState(false); + const [showEscalationEmail, setShowEscalationEmail] = useState(false); const isEdxStaff = getAuthenticatedUser().administrator; const [formStatus, setFormStatus] = useState({ isValid: true, @@ -53,6 +53,15 @@ const ProctoringSettings = ({ intl, onClose }) => { const isMobile = useIsMobile(); const modalVariant = isMobile ? 'dark' : 'default'; + const isLtiProvider = (provider) => ( + ltiProctoringProviders.some(p => p.name === provider) + ); + + function getProviderDisplayLabel(provider) { + // if a display label exists for this provider return it + return ltiProctoringProviders.find(p => p.name === provider)?.verbose_name || provider; + } + const { courseId } = useContext(PagesAndResourcesContext); const appInfo = useModel('courseApps', 'proctoring'); const alertRef = React.createRef(); @@ -73,38 +82,36 @@ const ProctoringSettings = ({ intl, onClose }) => { if (value === 'proctortrack') { setFormValues({ ...newFormValues, createZendeskTickets: false }); - setShowProctortrackEscalationEmail(true); + setShowEscalationEmail(true); + } else if (value === 'software_secure') { + setFormValues({ ...newFormValues, createZendeskTickets: true }); + setShowEscalationEmail(false); + } else if (isLtiProvider(value)) { + setFormValues(newFormValues); + setShowEscalationEmail(true); } else { - if (value === 'software_secure') { - setFormValues({ ...newFormValues, createZendeskTickets: true }); - } else { - setFormValues(newFormValues); - } - - setShowProctortrackEscalationEmail(false); + setFormValues(newFormValues); + setShowEscalationEmail(false); } } else { setFormValues({ ...formValues, [name]: value }); } }; - function isLtiProvider(provider) { - return ltiProctoringProviders.some(p => p.name === provider); - } - - const setFocusToProctortrackEscalationEmailInput = () => { + const setFocusToEscalationEmailInput = () => { if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) { proctoringEscalationEmailInputRef.current.focus(); } }; function postSettingsBackToServer() { - const providerIsLti = isLtiProvider(formValues.proctoringProvider); + const selectedProvider = formValues.proctoringProvider; + const isLtiProviderSelected = isLtiProvider(selectedProvider); const studioDataToPostBack = { proctored_exam_settings: { enable_proctored_exams: formValues.enableProctoredExams, // lti providers are managed outside edx-platform, lti_external indicates this - proctoring_provider: providerIsLti ? 'lti_external' : formValues.proctoringProvider, + proctoring_provider: isLtiProviderSelected ? 'lti_external' : selectedProvider, create_zendesk_tickets: formValues.createZendeskTickets, }, }; @@ -113,17 +120,23 @@ const ProctoringSettings = ({ intl, onClose }) => { } if (formValues.proctoringProvider === 'proctortrack') { - studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.proctortrackEscalationEmail === '' ? null : formValues.proctortrackEscalationEmail; + studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.escalationEmail === '' ? null : formValues.escalationEmail; } // only save back to exam service if necessary setSubmissionInProgress(true); + const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)]; if (allowLtiProviders && ExamsApiService.isAvailable()) { + const selectedEscalationEmail = formValues.escalationEmail; + saveOperations.push( ExamsApiService.saveCourseExamConfiguration( courseId, - { provider: providerIsLti ? formValues.proctoringProvider : null }, + { + provider: isLtiProviderSelected ? formValues.proctoringProvider : null, + escalationEmail: (isLtiProviderSelected && selectedEscalationEmail !== '') ? selectedEscalationEmail : null, + }, ), ); } @@ -141,20 +154,21 @@ const ProctoringSettings = ({ intl, onClose }) => { const handleSubmit = (event) => { event.preventDefault(); + const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider); if ( - formValues.proctoringProvider === 'proctortrack' - && !EmailValidator.validate(formValues.proctortrackEscalationEmail) - && !(formValues.proctortrackEscalationEmail === '' && !formValues.enableProctoredExams) + (formValues.proctoringProvider === 'proctortrack' || isLtiProviderSelected) + && !EmailValidator.validate(formValues.escalationEmail) + && !(formValues.escalationEmail === '' && !formValues.enableProctoredExams) ) { - if (formValues.proctortrackEscalationEmail === '') { - const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank']); + if (formValues.escalationEmail === '') { + const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank'], { proctoringProviderName: getProviderDisplayLabel(formValues.proctoringProvider) }); setFormStatus({ isValid: false, errors: { - formProctortrackEscalationEmail: { + formEscalationEmail: { dialogErrorMessage: ( - + {errorMessage} ), @@ -168,8 +182,8 @@ const ProctoringSettings = ({ intl, onClose }) => { setFormStatus({ isValid: false, errors: { - formProctortrackEscalationEmail: { - dialogErrorMessage: ({errorMessage}), + formEscalationEmail: { + dialogErrorMessage: ({errorMessage}), inputErrorMessage: errorMessage, }, }, @@ -178,7 +192,7 @@ const ProctoringSettings = ({ intl, onClose }) => { } else { postSettingsBackToServer(); const errors = { ...formStatus.errors }; - delete errors.formProctortrackEscalationEmail; + delete errors.formEscalationEmail; setFormStatus({ isValid: true, errors, @@ -202,11 +216,6 @@ const ProctoringSettings = ({ intl, onClose }) => { return markDisabled; } - function getProviderDisplayLabel(provider) { - // if a display label exists for this provider return it - return ltiProctoringProviders.find(p => p.name === provider)?.verbose_name || provider; - } - function getProctoringProviderOptions(providers) { return providers.map(provider => (