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.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { convertObjectToSnakeCase } from '../../utils';
|
||||||
|
|
||||||
class ExamsApiService {
|
class ExamsApiService {
|
||||||
static isAvailable() {
|
static isAvailable() {
|
||||||
@@ -26,8 +27,9 @@ class ExamsApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static saveCourseExamConfiguration(courseId, dataToSave) {
|
static saveCourseExamConfiguration(courseId, dataToSave) {
|
||||||
|
const snakecaseDataToSave = convertObjectToSnakeCase(dataToSave, true);
|
||||||
const apiClient = getAuthenticatedHttpClient();
|
const apiClient = getAuthenticatedHttpClient();
|
||||||
return apiClient.patch(this.getExamConfigurationUrl(courseId), dataToSave);
|
return apiClient.patch(this.getExamConfigurationUrl(courseId), snakecaseDataToSave);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
const initialFormValues = {
|
const initialFormValues = {
|
||||||
enableProctoredExams: false,
|
enableProctoredExams: false,
|
||||||
proctoringProvider: false,
|
proctoringProvider: false,
|
||||||
proctortrackEscalationEmail: '',
|
escalationEmail: '',
|
||||||
allowOptingOut: false,
|
allowOptingOut: false,
|
||||||
createZendeskTickets: false,
|
createZendeskTickets: false,
|
||||||
};
|
};
|
||||||
@@ -44,7 +44,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
const [saveError, setSaveError] = useState(false);
|
const [saveError, setSaveError] = useState(false);
|
||||||
const [submissionInProgress, setSubmissionInProgress] = useState(false);
|
const [submissionInProgress, setSubmissionInProgress] = useState(false);
|
||||||
const [showProctortrackEscalationEmail, setShowProctortrackEscalationEmail] = useState(false);
|
const [showEscalationEmail, setShowEscalationEmail] = useState(false);
|
||||||
const isEdxStaff = getAuthenticatedUser().administrator;
|
const isEdxStaff = getAuthenticatedUser().administrator;
|
||||||
const [formStatus, setFormStatus] = useState({
|
const [formStatus, setFormStatus] = useState({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
@@ -53,6 +53,15 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const modalVariant = isMobile ? 'dark' : 'default';
|
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 { courseId } = useContext(PagesAndResourcesContext);
|
||||||
const appInfo = useModel('courseApps', 'proctoring');
|
const appInfo = useModel('courseApps', 'proctoring');
|
||||||
const alertRef = React.createRef();
|
const alertRef = React.createRef();
|
||||||
@@ -73,38 +82,36 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
|
|
||||||
if (value === 'proctortrack') {
|
if (value === 'proctortrack') {
|
||||||
setFormValues({ ...newFormValues, createZendeskTickets: false });
|
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 {
|
} else {
|
||||||
if (value === 'software_secure') {
|
setFormValues(newFormValues);
|
||||||
setFormValues({ ...newFormValues, createZendeskTickets: true });
|
setShowEscalationEmail(false);
|
||||||
} else {
|
|
||||||
setFormValues(newFormValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowProctortrackEscalationEmail(false);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setFormValues({ ...formValues, [name]: value });
|
setFormValues({ ...formValues, [name]: value });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function isLtiProvider(provider) {
|
const setFocusToEscalationEmailInput = () => {
|
||||||
return ltiProctoringProviders.some(p => p.name === provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
const setFocusToProctortrackEscalationEmailInput = () => {
|
|
||||||
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
|
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
|
||||||
proctoringEscalationEmailInputRef.current.focus();
|
proctoringEscalationEmailInputRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function postSettingsBackToServer() {
|
function postSettingsBackToServer() {
|
||||||
const providerIsLti = isLtiProvider(formValues.proctoringProvider);
|
const selectedProvider = formValues.proctoringProvider;
|
||||||
|
const isLtiProviderSelected = isLtiProvider(selectedProvider);
|
||||||
const studioDataToPostBack = {
|
const studioDataToPostBack = {
|
||||||
proctored_exam_settings: {
|
proctored_exam_settings: {
|
||||||
enable_proctored_exams: formValues.enableProctoredExams,
|
enable_proctored_exams: formValues.enableProctoredExams,
|
||||||
// lti providers are managed outside edx-platform, lti_external indicates this
|
// 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,
|
create_zendesk_tickets: formValues.createZendeskTickets,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -113,17 +120,23 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (formValues.proctoringProvider === 'proctortrack') {
|
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
|
// only save back to exam service if necessary
|
||||||
setSubmissionInProgress(true);
|
setSubmissionInProgress(true);
|
||||||
|
|
||||||
const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)];
|
const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)];
|
||||||
if (allowLtiProviders && ExamsApiService.isAvailable()) {
|
if (allowLtiProviders && ExamsApiService.isAvailable()) {
|
||||||
|
const selectedEscalationEmail = formValues.escalationEmail;
|
||||||
|
|
||||||
saveOperations.push(
|
saveOperations.push(
|
||||||
ExamsApiService.saveCourseExamConfiguration(
|
ExamsApiService.saveCourseExamConfiguration(
|
||||||
courseId,
|
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) => {
|
const handleSubmit = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
|
||||||
if (
|
if (
|
||||||
formValues.proctoringProvider === 'proctortrack'
|
(formValues.proctoringProvider === 'proctortrack' || isLtiProviderSelected)
|
||||||
&& !EmailValidator.validate(formValues.proctortrackEscalationEmail)
|
&& !EmailValidator.validate(formValues.escalationEmail)
|
||||||
&& !(formValues.proctortrackEscalationEmail === '' && !formValues.enableProctoredExams)
|
&& !(formValues.escalationEmail === '' && !formValues.enableProctoredExams)
|
||||||
) {
|
) {
|
||||||
if (formValues.proctortrackEscalationEmail === '') {
|
if (formValues.escalationEmail === '') {
|
||||||
const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank']);
|
const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank'], { proctoringProviderName: getProviderDisplayLabel(formValues.proctoringProvider) });
|
||||||
|
|
||||||
setFormStatus({
|
setFormStatus({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errors: {
|
errors: {
|
||||||
formProctortrackEscalationEmail: {
|
formEscalationEmail: {
|
||||||
dialogErrorMessage: (
|
dialogErrorMessage: (
|
||||||
<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">
|
<Alert.Link onClick={setFocusToEscalationEmailInput} href="#formEscalationEmail" data-testid="escalationEmailErrorLink">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Alert.Link>
|
</Alert.Link>
|
||||||
),
|
),
|
||||||
@@ -168,8 +182,8 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
setFormStatus({
|
setFormStatus({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errors: {
|
errors: {
|
||||||
formProctortrackEscalationEmail: {
|
formEscalationEmail: {
|
||||||
dialogErrorMessage: (<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">{errorMessage}</Alert.Link>),
|
dialogErrorMessage: (<Alert.Link onClick={setFocusToEscalationEmailInput} href="#formEscalationEmail" data-testid="escalationEmailErrorLink">{errorMessage}</Alert.Link>),
|
||||||
inputErrorMessage: errorMessage,
|
inputErrorMessage: errorMessage,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -178,7 +192,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
} else {
|
} else {
|
||||||
postSettingsBackToServer();
|
postSettingsBackToServer();
|
||||||
const errors = { ...formStatus.errors };
|
const errors = { ...formStatus.errors };
|
||||||
delete errors.formProctortrackEscalationEmail;
|
delete errors.formEscalationEmail;
|
||||||
setFormStatus({
|
setFormStatus({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
errors,
|
errors,
|
||||||
@@ -202,11 +216,6 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
return markDisabled;
|
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) {
|
function getProctoringProviderOptions(providers) {
|
||||||
return providers.map(provider => (
|
return providers.map(provider => (
|
||||||
<option
|
<option
|
||||||
@@ -247,16 +256,18 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
|
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!formStatus.isValid && formStatus.errors.formProctortrackEscalationEmail
|
{!formStatus.isValid && formStatus.errors.formEscalationEmail
|
||||||
&& (
|
&& (
|
||||||
// tabIndex="-1" to make non-focusable element focusable
|
// tabIndex="-1" to make non-focusable element focusable
|
||||||
<Alert
|
<Alert
|
||||||
id="proctortrackEscalationEmailError"
|
id="escalationEmailError"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
data-testid="proctortrackEscalationEmailError"
|
data-testid="escalationEmailError"
|
||||||
ref={alertRef}
|
ref={alertRef}
|
||||||
>
|
>
|
||||||
{getFormErrorMessage()}
|
{getFormErrorMessage()}
|
||||||
@@ -319,30 +330,30 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PROCTORTRACK ESCALATION EMAIL */}
|
{/* ESCALATION EMAIL */}
|
||||||
{showProctortrackEscalationEmail && formValues.enableProctoredExams && (
|
{showEscalationEmail && formValues.enableProctoredExams && (
|
||||||
<Form.Group controlId="formProctortrackEscalationEmail">
|
<Form.Group controlId="formEscalationEmail">
|
||||||
<Form.Label className="font-weight-bold">
|
<Form.Label className="font-weight-bold">
|
||||||
{intl.formatMessage(messages['authoring.proctoring.escalationemail.label'])}
|
{intl.formatMessage(messages['authoring.proctoring.escalationemail.label'])}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
ref={proctoringEscalationEmailInputRef}
|
ref={proctoringEscalationEmailInputRef}
|
||||||
type="email"
|
type="email"
|
||||||
name="proctortrackEscalationEmail"
|
name="escalationEmail"
|
||||||
data-testid="escalationEmail"
|
data-testid="escalationEmail"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={formValues.proctortrackEscalationEmail}
|
value={formValues.escalationEmail}
|
||||||
isInvalid={Object.prototype.hasOwnProperty.call(formStatus.errors, 'formProctortrackEscalationEmail')}
|
isInvalid={Object.prototype.hasOwnProperty.call(formStatus.errors, 'formEscalationEmail')}
|
||||||
aria-describedby="proctortrackEscalationEmailHelpText"
|
aria-describedby="escalationEmailHelpText"
|
||||||
/>
|
/>
|
||||||
<Form.Text id="proctortrackEscalationEmailHelpText">
|
<Form.Text id="escalationEmailHelpText">
|
||||||
{intl.formatMessage(messages['authoring.proctoring.escalationemail.help'])}
|
{intl.formatMessage(messages['authoring.proctoring.escalationemail.help'])}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
{Object.prototype.hasOwnProperty.call(formStatus.errors, 'formProctortrackEscalationEmail') && (
|
{Object.prototype.hasOwnProperty.call(formStatus.errors, 'formEscalationEmail') && (
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{
|
{
|
||||||
formStatus.errors.formProctortrackEscalationEmail
|
formStatus.errors.formEscalationEmail
|
||||||
&& formStatus.errors.formProctortrackEscalationEmail.inputErrorMessage
|
&& formStatus.errors.formEscalationEmail.inputErrorMessage
|
||||||
}
|
}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
)}
|
)}
|
||||||
@@ -350,7 +361,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ALLOW OPTING OUT OF PROCTORED EXAMS */}
|
{/* ALLOW OPTING OUT OF PROCTORED EXAMS */}
|
||||||
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && (
|
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
|
||||||
<fieldset aria-describedby="allowOptingOutHelpText">
|
<fieldset aria-describedby="allowOptingOutHelpText">
|
||||||
<Form.Group controlId="formAllowingOptingOut">
|
<Form.Group controlId="formAllowingOptingOut">
|
||||||
<Form.Label as="legend" className="font-weight-bold">
|
<Form.Label as="legend" className="font-weight-bold">
|
||||||
@@ -374,7 +385,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* CREATE ZENDESK TICKETS */}
|
{/* CREATE ZENDESK TICKETS */}
|
||||||
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && (
|
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
|
||||||
<fieldset aria-describedby="createZendeskTicketsText">
|
<fieldset aria-describedby="createZendeskTicketsText">
|
||||||
<Form.Group controlId="formCreateZendeskTickets">
|
<Form.Group controlId="formCreateZendeskTickets">
|
||||||
<Form.Label as="legend" className="font-weight-bold">
|
<Form.Label as="legend" className="font-weight-bold">
|
||||||
@@ -487,10 +498,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
setSubmissionInProgress(false);
|
setSubmissionInProgress(false);
|
||||||
setCourseStartDate(settingsResponse.data.course_start_date);
|
setCourseStartDate(settingsResponse.data.course_start_date);
|
||||||
const isProctortrack = proctoredExamSettings.proctoring_provider === 'proctortrack';
|
|
||||||
setShowProctortrackEscalationEmail(isProctortrack);
|
|
||||||
setAvailableProctoringProviders(settingsResponse.data.available_proctoring_providers);
|
setAvailableProctoringProviders(settingsResponse.data.available_proctoring_providers);
|
||||||
const proctoringEscalationEmail = proctoredExamSettings.proctoring_escalation_email;
|
|
||||||
|
|
||||||
// The list of providers returned by studio settings are the default behavior. If lti_external
|
// The list of providers returned by studio settings are the default behavior. If lti_external
|
||||||
// is available as an option display the list of LTI providers returned by the exam service.
|
// is available as an option display the list of LTI providers returned by the exam service.
|
||||||
@@ -517,6 +525,18 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
} else {
|
} else {
|
||||||
selectedProvider = proctoredExamSettings.proctoring_provider;
|
selectedProvider = proctoredExamSettings.proctoring_provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isProctortrack = selectedProvider === 'proctortrack';
|
||||||
|
const ltiProviderSelected = proctoringProvidersLti.some(p => p.name === selectedProvider);
|
||||||
|
|
||||||
|
if (isProctortrack || ltiProviderSelected) {
|
||||||
|
setShowEscalationEmail(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proctoringEscalationEmail = ltiProviderSelected
|
||||||
|
? examConfigResponse.data.escalation_email
|
||||||
|
: proctoredExamSettings.proctoring_escalation_email;
|
||||||
|
|
||||||
setFormValues({
|
setFormValues({
|
||||||
...formValues,
|
...formValues,
|
||||||
proctoringProvider: selectedProvider,
|
proctoringProvider: selectedProvider,
|
||||||
@@ -526,7 +546,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
|||||||
// The backend API may return null for the proctoringEscalationEmail value, which is the default.
|
// The backend API may return null for the proctoringEscalationEmail value, which is the default.
|
||||||
// In order to keep our email input component controlled, we use the empty string as the default
|
// In order to keep our email input component controlled, we use the empty string as the default
|
||||||
// and perform this conversion during GETs and POSTs.
|
// and perform this conversion during GETs and POSTs.
|
||||||
proctortrackEscalationEmail: proctoringEscalationEmail === null ? '' : proctoringEscalationEmail,
|
escalationEmail: proctoringEscalationEmail === null ? '' : proctoringEscalationEmail,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
).catch(
|
).catch(
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ describe('ProctoredExamSettings', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
|
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
|
||||||
});
|
});
|
||||||
expect(screen.queryByTestId('escalationEmail')).toBeNull();
|
|
||||||
expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull();
|
expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull();
|
||||||
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
|
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
|
||||||
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
|
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
|
||||||
@@ -204,6 +203,8 @@ describe('ProctoredExamSettings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Validation with invalid escalation email', () => {
|
describe('Validation with invalid escalation email', () => {
|
||||||
|
const proctoringProvidersRequiringEscalationEmail = ['proctortrack', 'test_lti'];
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
axiosMock.onGet(
|
axiosMock.onGet(
|
||||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||||
@@ -215,10 +216,14 @@ describe('ProctoredExamSettings', () => {
|
|||||||
proctoring_escalation_email: 'test@example.com',
|
proctoring_escalation_email: 'test@example.com',
|
||||||
create_zendesk_tickets: true,
|
create_zendesk_tickets: true,
|
||||||
},
|
},
|
||||||
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
|
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc', 'lti_external'],
|
||||||
course_start_date: '2070-01-01T00:00:00Z',
|
course_start_date: '2070-01-01T00:00:00Z',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
axiosMock.onPatch(
|
||||||
|
ExamsApiService.getExamConfigurationUrl(defaultProps.courseId),
|
||||||
|
).reply(204, {});
|
||||||
|
|
||||||
axiosMock.onPost(
|
axiosMock.onPost(
|
||||||
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
|
||||||
).reply(200, {});
|
).reply(200, {});
|
||||||
@@ -226,175 +231,183 @@ describe('ProctoredExamSettings', () => {
|
|||||||
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
|
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Creates an alert when no proctoring escalation email is provided with proctortrack selected', async () => {
|
proctoringProvidersRequiringEscalationEmail.forEach(provider => {
|
||||||
await waitFor(() => {
|
it(`Creates an alert when no proctoring escalation email is provided with ${provider} selected`, async () => {
|
||||||
screen.getByDisplayValue('proctortrack');
|
await waitFor(() => {
|
||||||
});
|
screen.getByDisplayValue('proctortrack');
|
||||||
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
});
|
||||||
await act(async () => {
|
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
||||||
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
|
await act(async () => {
|
||||||
});
|
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
|
||||||
const selectButton = screen.getByTestId('submissionButton');
|
});
|
||||||
await act(async () => {
|
const selectButton = screen.getByTestId('submissionButton');
|
||||||
fireEvent.click(selectButton);
|
await act(async () => {
|
||||||
|
fireEvent.click(selectButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify alert content and focus management
|
||||||
|
const escalationEmailError = screen.getByTestId('escalationEmailError');
|
||||||
|
expect(escalationEmailError.textContent).not.toBeNull();
|
||||||
|
expect(document.activeElement).toEqual(escalationEmailError);
|
||||||
|
|
||||||
|
// verify alert link links to offending input
|
||||||
|
const errorLink = screen.getByTestId('escalationEmailErrorLink');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(errorLink);
|
||||||
|
});
|
||||||
|
const escalationEmailInput = screen.getByTestId('escalationEmail');
|
||||||
|
expect(document.activeElement).toEqual(escalationEmailInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
// verify alert content and focus management
|
it(`Creates an alert when invalid proctoring escalation email is provided with ${provider} selected`, async () => {
|
||||||
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError');
|
await waitFor(() => {
|
||||||
expect(escalationEmailError.textContent).not.toBeNull();
|
screen.getByDisplayValue('proctortrack');
|
||||||
expect(document.activeElement).toEqual(escalationEmailError);
|
});
|
||||||
|
|
||||||
// verify alert link links to offending input
|
const selectElement = screen.getByDisplayValue('proctortrack');
|
||||||
const errorLink = screen.getByTestId('proctorTrackEscalationEmailErrorLink');
|
await act(async () => {
|
||||||
await act(async () => {
|
fireEvent.change(selectElement, { target: { value: provider } });
|
||||||
fireEvent.click(errorLink);
|
});
|
||||||
});
|
|
||||||
const escalationEmailInput = screen.getByTestId('escalationEmail');
|
|
||||||
expect(document.activeElement).toEqual(escalationEmailInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Creates an alert when invalid proctoring escalation email is provided with proctortrack selected', async () => {
|
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
||||||
await waitFor(() => {
|
await act(async () => {
|
||||||
screen.getByDisplayValue('proctortrack');
|
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
|
||||||
});
|
});
|
||||||
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
const selectButton = screen.getByTestId('submissionButton');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
|
fireEvent.click(selectButton);
|
||||||
});
|
});
|
||||||
const selectButton = screen.getByTestId('submissionButton');
|
|
||||||
await act(async () => {
|
// verify alert content and focus management
|
||||||
fireEvent.click(selectButton);
|
const escalationEmailError = screen.getByTestId('escalationEmailError');
|
||||||
|
expect(document.activeElement).toEqual(escalationEmailError);
|
||||||
|
expect(escalationEmailError.textContent).not.toBeNull();
|
||||||
|
expect(document.activeElement).toEqual(escalationEmailError);
|
||||||
|
|
||||||
|
// verify alert link links to offending input
|
||||||
|
const errorLink = screen.getByTestId('escalationEmailErrorLink');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(errorLink);
|
||||||
|
});
|
||||||
|
const escalationEmailInput = screen.getByTestId('escalationEmail');
|
||||||
|
expect(document.activeElement).toEqual(escalationEmailInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
// verify alert content and focus management
|
it('Creates an alert when invalid proctoring escalation email is provided with proctoring disabled', async () => {
|
||||||
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError');
|
await waitFor(() => {
|
||||||
expect(document.activeElement).toEqual(escalationEmailError);
|
screen.getByDisplayValue('proctortrack');
|
||||||
expect(escalationEmailError.textContent).not.toBeNull();
|
});
|
||||||
expect(document.activeElement).toEqual(escalationEmailError);
|
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
|
||||||
|
});
|
||||||
|
const enableProctoringElement = screen.getByText('Proctored exams');
|
||||||
|
await act(async () => fireEvent.click(enableProctoringElement));
|
||||||
|
const selectButton = screen.getByTestId('submissionButton');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(selectButton);
|
||||||
|
});
|
||||||
|
|
||||||
// verify alert link links to offending input
|
// verify alert content and focus management
|
||||||
const errorLink = screen.getByTestId('proctorTrackEscalationEmailErrorLink');
|
const escalationEmailError = screen.getByTestId('escalationEmailError');
|
||||||
await act(async () => {
|
expect(document.activeElement).toEqual(escalationEmailError);
|
||||||
fireEvent.click(errorLink);
|
expect(escalationEmailError.textContent).not.toBeNull();
|
||||||
});
|
expect(document.activeElement).toEqual(escalationEmailError);
|
||||||
const escalationEmailInput = screen.getByTestId('escalationEmail');
|
|
||||||
expect(document.activeElement).toEqual(escalationEmailInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Creates an alert when invalid proctoring escalation email is provided with proctoring disabled', async () => {
|
|
||||||
await waitFor(() => {
|
|
||||||
screen.getByDisplayValue('proctortrack');
|
|
||||||
});
|
|
||||||
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
|
|
||||||
});
|
|
||||||
const enableProctoringElement = screen.getByText('Proctored exams');
|
|
||||||
await act(async () => fireEvent.click(enableProctoringElement));
|
|
||||||
const selectButton = screen.getByTestId('submissionButton');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.click(selectButton);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// verify alert content and focus management
|
it('Has no error when empty proctoring escalation email is provided with proctoring disabled', async () => {
|
||||||
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError');
|
await waitFor(() => {
|
||||||
expect(document.activeElement).toEqual(escalationEmailError);
|
screen.getByDisplayValue('proctortrack');
|
||||||
expect(escalationEmailError.textContent).not.toBeNull();
|
});
|
||||||
expect(document.activeElement).toEqual(escalationEmailError);
|
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
||||||
});
|
await act(async () => {
|
||||||
|
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
|
||||||
|
});
|
||||||
|
const enableProctoringElement = screen.getByText('Proctored exams');
|
||||||
|
await act(async () => fireEvent.click(enableProctoringElement));
|
||||||
|
const selectButton = screen.getByTestId('submissionButton');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(selectButton);
|
||||||
|
});
|
||||||
|
|
||||||
it('Has no error when invalid proctoring escalation email is provided with proctoring disabled', async () => {
|
// verify there is no escalation email alert, and focus has been set on save success alert
|
||||||
await waitFor(() => {
|
expect(screen.queryByTestId('escalationEmailError')).toBeNull();
|
||||||
screen.getByDisplayValue('proctortrack');
|
|
||||||
});
|
const errorAlert = screen.getByTestId('saveSuccess');
|
||||||
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
expect(errorAlert.textContent).toEqual(
|
||||||
await act(async () => {
|
expect.stringContaining('Proctored exam settings saved successfully.'),
|
||||||
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
|
);
|
||||||
});
|
expect(document.activeElement).toEqual(errorAlert);
|
||||||
const enableProctoringElement = screen.getByText('Proctored exams');
|
|
||||||
await act(async () => fireEvent.click(enableProctoringElement));
|
|
||||||
const selectButton = screen.getByTestId('submissionButton');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.click(selectButton);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// verify there is no escalation email alert, and focus has been set on save success alert
|
it(`Has no error when valid proctoring escalation email is provided with ${provider} selected`, async () => {
|
||||||
expect(screen.queryByTestId('proctortrackEscalationEmailError')).toBeNull();
|
await waitFor(() => {
|
||||||
|
screen.getByDisplayValue('proctortrack');
|
||||||
|
});
|
||||||
|
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
|
||||||
|
});
|
||||||
|
const selectButton = screen.getByTestId('submissionButton');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(selectButton);
|
||||||
|
});
|
||||||
|
|
||||||
const errorAlert = screen.getByTestId('saveSuccess');
|
// verify there is no escalation email alert, and focus has been set on save success alert
|
||||||
expect(errorAlert.textContent).toEqual(
|
expect(screen.queryByTestId('escalationEmailError')).toBeNull();
|
||||||
expect.stringContaining('Proctored exam settings saved successfully.'),
|
|
||||||
);
|
|
||||||
expect(document.activeElement).toEqual(errorAlert);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Has no error when valid proctoring escalation email is provided with proctortrack selected', async () => {
|
const errorAlert = screen.getByTestId('saveSuccess');
|
||||||
await waitFor(() => {
|
expect(errorAlert.textContent).toEqual(
|
||||||
screen.getByDisplayValue('proctortrack');
|
expect.stringContaining('Proctored exam settings saved successfully.'),
|
||||||
});
|
);
|
||||||
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
expect(document.activeElement).toEqual(errorAlert);
|
||||||
await act(async () => {
|
|
||||||
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
|
|
||||||
});
|
|
||||||
const selectButton = screen.getByTestId('submissionButton');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.click(selectButton);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// verify there is no escalation email alert, and focus has been set on save success alert
|
it(`Escalation email field hidden when proctoring backend is not ${provider}`, async () => {
|
||||||
expect(screen.queryByTestId('proctortrackEscalationEmailError')).toBeNull();
|
await waitFor(() => {
|
||||||
|
screen.getByDisplayValue('proctortrack');
|
||||||
|
});
|
||||||
|
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
|
||||||
|
const selectEscalationEmailElement = screen.getByTestId('escalationEmail');
|
||||||
|
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId('escalationEmail')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
const errorAlert = screen.getByTestId('saveSuccess');
|
it(`Escalation email Field Show when proctoring backend is switched back to ${provider}`, async () => {
|
||||||
expect(errorAlert.textContent).toEqual(
|
await waitFor(() => {
|
||||||
expect.stringContaining('Proctored exam settings saved successfully.'),
|
screen.getByDisplayValue('proctortrack');
|
||||||
);
|
});
|
||||||
expect(document.activeElement).toEqual(errorAlert);
|
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
|
||||||
});
|
let selectEscalationEmailElement = screen.getByTestId('escalationEmail');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId('escalationEmail')).toBeNull();
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
|
||||||
|
selectEscalationEmailElement = screen.getByTestId('escalationEmail');
|
||||||
|
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
it('Escalation email field hidden when proctoring backend is not Proctortrack', async () => {
|
it('Submits form when "Enter" key is hit in the escalation email field', async () => {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
screen.getByDisplayValue('proctortrack');
|
screen.getByDisplayValue('proctortrack');
|
||||||
|
});
|
||||||
|
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.submit(selectEscalationEmailElement);
|
||||||
|
});
|
||||||
|
// if the error appears, the form has been submitted
|
||||||
|
expect(screen.getByTestId('escalationEmailError')).toBeDefined();
|
||||||
});
|
});
|
||||||
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
|
|
||||||
const selectEscalationEmailElement = screen.getByTestId('escalationEmail');
|
|
||||||
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
|
|
||||||
});
|
|
||||||
expect(screen.queryByTestId('escalationEmail')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Escalation email Field Show when proctoring backend is switched back to Proctortrack', async () => {
|
|
||||||
await waitFor(() => {
|
|
||||||
screen.getByDisplayValue('proctortrack');
|
|
||||||
});
|
|
||||||
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
|
|
||||||
let selectEscalationEmailElement = screen.getByTestId('escalationEmail');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
|
|
||||||
});
|
|
||||||
expect(screen.queryByTestId('escalationEmail')).toBeNull();
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
|
|
||||||
});
|
|
||||||
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
|
|
||||||
selectEscalationEmailElement = screen.getByTestId('escalationEmail');
|
|
||||||
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Submits form when "Enter" key is hit in the escalation email field', async () => {
|
|
||||||
await waitFor(() => {
|
|
||||||
screen.getByDisplayValue('proctortrack');
|
|
||||||
});
|
|
||||||
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
|
|
||||||
});
|
|
||||||
await act(async () => {
|
|
||||||
fireEvent.submit(selectEscalationEmailElement);
|
|
||||||
});
|
|
||||||
// if the error appears, the form has been submitted
|
|
||||||
expect(screen.getByTestId('proctortrackEscalationEmailError')).toBeDefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -687,11 +700,19 @@ describe('ProctoredExamSettings', () => {
|
|||||||
|
|
||||||
it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => {
|
it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => {
|
||||||
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
|
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
|
||||||
// Make a change to the provider to proctortrack and set the email
|
// Make a change to the provider to test_lti and set the email
|
||||||
const selectElement = screen.getByDisplayValue('mockproc');
|
const selectElement = screen.getByDisplayValue('mockproc');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
|
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const escalationEmail = screen.getByTestId('escalationEmail');
|
||||||
|
expect(escalationEmail.value).toEqual('test@example.com');
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } });
|
||||||
|
});
|
||||||
|
expect(escalationEmail.value).toEqual('test_lti@example.com');
|
||||||
|
|
||||||
const submitButton = screen.getByTestId('submissionButton');
|
const submitButton = screen.getByTestId('submissionButton');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
@@ -701,6 +722,7 @@ describe('ProctoredExamSettings', () => {
|
|||||||
expect(axiosMock.history.patch.length).toBe(1);
|
expect(axiosMock.history.patch.length).toBe(1);
|
||||||
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
||||||
provider: 'test_lti',
|
provider: 'test_lti',
|
||||||
|
escalation_email: 'test_lti@example.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
// update studio settings
|
// update studio settings
|
||||||
@@ -731,6 +753,7 @@ describe('ProctoredExamSettings', () => {
|
|||||||
expect(axiosMock.history.patch.length).toBe(1);
|
expect(axiosMock.history.patch.length).toBe(1);
|
||||||
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
||||||
provider: null,
|
provider: null,
|
||||||
|
escalation_email: null,
|
||||||
});
|
});
|
||||||
expect(axiosMock.history.patch.length).toBe(1);
|
expect(axiosMock.history.patch.length).toBe(1);
|
||||||
expect(axiosMock.history.post.length).toBe(1);
|
expect(axiosMock.history.post.length).toBe(1);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
'authoring.proctoring.escalationemail.label': {
|
'authoring.proctoring.escalationemail.label': {
|
||||||
id: 'authoring.proctoring.escalationemail.label',
|
id: 'authoring.proctoring.escalationemail.label',
|
||||||
defaultMessage: 'Proctortrack escalation email',
|
defaultMessage: 'Escalation email',
|
||||||
description: 'Label for escalation email text field',
|
description: 'Label for escalation email text field',
|
||||||
},
|
},
|
||||||
'authoring.proctoring.escalationemail.help': {
|
'authoring.proctoring.escalationemail.help': {
|
||||||
@@ -63,12 +63,12 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
'authoring.proctoring.escalationemail.error.blank': {
|
'authoring.proctoring.escalationemail.error.blank': {
|
||||||
id: 'authoring.proctoring.escalationemail.error.blank',
|
id: 'authoring.proctoring.escalationemail.error.blank',
|
||||||
defaultMessage: 'The Proctortrack Escalation Email field cannot be empty if proctortrack is the selected provider.',
|
defaultMessage: 'The Escalation Email field cannot be empty if {proctoringProviderName} is the selected provider.',
|
||||||
description: 'Error message for missing required email field.',
|
description: 'Error message for missing required email field.',
|
||||||
},
|
},
|
||||||
'authoring.proctoring.escalationemail.error.invalid': {
|
'authoring.proctoring.escalationemail.error.invalid': {
|
||||||
id: 'authoring.proctoring.escalationemail.error.invalid',
|
id: 'authoring.proctoring.escalationemail.error.invalid',
|
||||||
defaultMessage: 'The Proctortrack Escalation Email field is in the wrong format and is not valid.',
|
defaultMessage: 'The Escalation Email field is in the wrong format and is not valid.',
|
||||||
description: 'Error message for a invalid email format.',
|
description: 'Error message for a invalid email format.',
|
||||||
},
|
},
|
||||||
'authoring.proctoring.allowoptout.label': {
|
'authoring.proctoring.allowoptout.label': {
|
||||||
|
|||||||
Reference in New Issue
Block a user