Skip to content

Commit

Permalink
- feat: Improved constants and messages
Browse files Browse the repository at this point in the history
  • Loading branch information
ilee2u committed Aug 3, 2023
1 parent 980ccc4 commit 92fafc4
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 146 deletions.
28 changes: 28 additions & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ export const RequestKeys = {
modifyExamAttempt: 'modifyExamAttempt',
};

export const ExamAttemptActions = {
// NOTE: These are the "staff-only" actions, which are meant to be performed from this dashboard.
verify: 'verify',
reject: 'reject',
};

export const ExamAttemptStatus = {
created: 'Created',
download_software_clicked: 'Download Software Clicked',
ready_to_start: 'Ready To Start',
started: 'Started',
ready_to_submit: 'Ready To Submit',
timed_out: 'Timed Out',
submitted: 'Submitted',
verified: 'Verified',
rejected: 'Rejected',
expired: 'Expired',
second_review_required: 'Second Review Required',
error: 'Error',
};

export const ExamTypes = {
proctored: 'Proctored',
timed: 'Timed',
practice: 'Practice',
onboarding: 'Onboarding',
};

export const ErrorStatuses = {
badRequest: 400,
unauthorized: 401,
Expand Down
95 changes: 20 additions & 75 deletions src/pages/ExamsPage/components/AttemptList.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import PropTypes from 'prop-types';
import { DataTable } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import * as constants from 'data/constants';
import ResetExamAttemptButton from './ResetExamAttemptButton';
import ReviewExamAttemptButton from './ReviewExamAttemptButton';

const cleanString = (string) => {
// Remove underscores, capitalize the first letter of each word, and return as a single string.
return string.split("_").map(string => string.charAt(0).toUpperCase() + string.slice(1)).join(" ");
};

// (string.charAt(0).toUpperCase() + string.slice(1)).replace(/_/g, " ");
import messages from '../messages';

// The button components must be compartmentalized here otherwise npm lint throws an unstable-nested-component error.
const ResetButton = (row) => (
Expand All @@ -20,21 +15,16 @@ const ResetButton = (row) => (
/>
);

const ReviewButton = (row, attempts) => (
const ReviewButton = (row) => (
<ReviewExamAttemptButton
username={row.original.username}
examName={row.original.exam_name}
attemptId={row.original.attempt_id}
severity={row.original.severity}
submissionReason={row.original.submission_reason}
/>
);

const getAndReadAttempts = (attempts) => {
if (attempts !== undefined) {
return attempts;
}
return [];
};

const AttemptList = ({ attempts }) => {
const { formatMessage, formatDate } = useIntl();

Expand All @@ -51,67 +41,35 @@ const AttemptList = ({ attempts }) => {
additionalColumns={[
{
id: 'action',
Header: formatMessage({
id: 'AttemptsList.action',
defaultMessage: 'Action',
description: 'Table header for the table column listing action to reset the exam attempt',
}),
Header: formatMessage(messages.examAttemptsTableHeaderAction),
Cell: ({ row }) => ResetButton(row),
},
{
id: 'review',
Header: formatMessage({
id: 'AttemptsList.review',
defaultMessage: 'Review',
description: 'Table header for the table column listing review to reset the exam attempt',
}),
Cell: ({ row }) => (row.original.status === "second_review_required" ? ReviewButton(row, attempts) : null),
Header: formatMessage(messages.examAttemptsTableHeaderReview),
Cell: ({ row }) => (row.original.status === 'second_review_required' ? ReviewButton(row) : null),
},
]}
data={getAndReadAttempts(attempts)}
data={attempts}
columns={[
{
Header: formatMessage({
id: 'AttemptsList.exam_name',
defaultMessage: 'Exam Name',
description: 'Table header for the table column listing the exam name',
}),
Header: formatMessage(messages.examAttemptsTableHeaderExamName),
accessor: 'exam_name',
},
{
Header: formatMessage({
id: 'AttemptsList.username',
defaultMessage: 'Username',
description: 'Table header for the table column listing the username',
}),
Header: formatMessage(messages.examAttemptsTableHeaderUsername),
accessor: 'username',
},
{
Header: formatMessage({
id: 'AttemptsList.time_limit',
defaultMessage: 'Time Limit',
description: 'Table header for the table column listing the time limit to complete the exam',
}),
Cell: ({ row }) => formatMessage({
id: 'AttemptsList.time_limit',
defaultMessage: `${row.original.time_limit} minutes`,
description: 'Data cell for the time limit to complete the exam',
}),
Header: formatMessage(messages.examAttemptsTableHeaderTimeLimit),
Cell: ({ row }) => (row.original.time_limit),
},
{
Header: formatMessage({
id: 'AttemptsList.exam_type',
defaultMessage: 'Exam Type',
description: 'Table header for the type of the exam',
}),
Cell: ({ row }) => (cleanString(row.original.exam_type)),
Header: formatMessage(messages.examAttemptsTableHeaderExamType),
Cell: ({ row }) => constants.ExamTypes[row.original.exam_type],
},
{
Header: formatMessage({
id: 'AttemptsList.started_at',
defaultMessage: 'Started At',
description: 'Table header for the time the exam attempt was started',
}),
Header: formatMessage(messages.examAttemptsTableHeaderStartedAt),
Cell: ({ row }) => (formatDate(row.original.started_at, {
year: 'numeric',
month: 'numeric',
Expand All @@ -121,11 +79,7 @@ const AttemptList = ({ attempts }) => {
})),
},
{
Header: formatMessage({
id: 'AttemptsList.completed_at',
defaultMessage: 'Completed At',
description: 'Table header for the time the exam attempt was completed',
}),
Header: formatMessage(messages.examAttemptsTableHeaderCompletedAt),
Cell: ({ row }) => (formatDate(row.original.completed_at, {
year: 'numeric',
month: 'numeric',
Expand All @@ -135,23 +89,14 @@ const AttemptList = ({ attempts }) => {
})),
},
{
Header: formatMessage({
id: 'AttemptsList.status',
defaultMessage: 'Status',
description: 'Table header for the current status of the exam attempt',
}),
Cell: ({ row }) => cleanString(row.original.status),
Header: formatMessage(messages.examAttemptsTableHeaderStatus),
Cell: ({ row }) => constants.ExamAttemptStatus[row.original.status],
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content={formatMessage({
id: 'AttemptsList.DataTable.EmptyTable',
defaultMessage: 'No results found.',
description: 'Message that appears in the table if no data is found',
})}
/>
<DataTable.EmptyTable content={formatMessage(messages.examAttemptsTableHeaderEmptyTable)} />
<DataTable.TableFooter />
</DataTable>
</div>
Expand Down
40 changes: 12 additions & 28 deletions src/pages/ExamsPage/components/ResetExamAttemptButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useDeleteExamAttempt } from '../hooks';
import messages from '../messages';

const ResetExamAttemptButton = ({ username, examName, attemptId }) => {
const [isOpen, open, close] = useToggle(false);
Expand All @@ -15,11 +16,8 @@ const ResetExamAttemptButton = ({ username, examName, attemptId }) => {
<>
<div className="d-flex">
<Button variant="link" size="sm" onClick={open}>
{formatMessage({
id: 'ResetExamAttemptButton.exam_name',
defaultMessage: 'Reset',
description: 'Table header for the table column with buttons to reset exam attempts',
})}
{/* TODO: Figure out why this has an extra semicolon on it by default */}
{formatMessage(messages.ResetExamAttemptButtonTitle)};
</Button>
</div>
<ModalDialog
Expand All @@ -33,45 +31,31 @@ const ResetExamAttemptButton = ({ username, examName, attemptId }) => {
>
<ModalDialog.Header>
<ModalDialog.Title>
{formatMessage({
id: 'ResetExamAttemptButton.confirmation_modal_title',
defaultMessage: 'Please confirm your choice.',
description: 'Title header of the modal that appears to confirm the reset of an exam attempt',
})}
{formatMessage(messages.ResetExamAttemptButtonModalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>

<ModalDialog.Body>
{formatMessage(
{
id: 'ResetExamAttemptButton.confirmation_modal_body',
defaultMessage: 'Are you sure you want to remove the exam attempt for learner with username "{username}" for the exam "{examName}"?.',
description: 'Body text of the modal that appears to confirm the reset of an exam attempt',
},
{ username, examName },
)}
{/* TODO: Figure out how to move this while keeping the vars passed in */}
<p>{formatMessage(messages.ResetExamAttemptButtonModalBody)}</p>
<ul>
<li>{formatMessage(messages.Username)}{username}</li>
<li>{formatMessage(messages.ExamName)}{examName}</li>
</ul>
</ModalDialog.Body>

<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{formatMessage({
id: 'ResetExamAttemptButton.cancel_button',
defaultMessage: 'No (Cancel)',
description: 'Text for the button to cancel resetting an exam attempt',
})}
{formatMessage(messages.ResetExamAttemptButtonCancel)}
</ModalDialog.CloseButton>
<Button
variant="primary"
onClick={e => { // eslint-disable-line no-unused-vars
resetExamAttempt(attemptId);
}}
>
{formatMessage({
id: 'ResetExamAttemptButton.confirm_button',
defaultMessage: 'Yes, I\'m Sure',
description: 'Text for the button to confirm the reset of an exam attempt',
})}
{formatMessage(messages.ResetExamAttemptButtonConfirm)}
</Button>
</ActionRow>
</ModalDialog.Footer>
Expand Down
52 changes: 17 additions & 35 deletions src/pages/ExamsPage/components/ReviewExamAttemptButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
import { useIntl } from '@edx/frontend-platform/i18n';
import { Warning } from '@edx/paragon/icons';
import { useModifyExamAttempt } from '../hooks';
import messages from '../messages';

const ReviewExamAttemptButton = ({
username, examName, attemptId, suspicionLevel,
username, examName, attemptId, severity, submissionReason,
}) => {
const [isOpen, open, close] = useToggle(false);
const modifyExamAttempt = useModifyExamAttempt();
Expand All @@ -19,11 +20,7 @@ const ReviewExamAttemptButton = ({
<div className="d-flex text-danger">
<Button variant="link" size="sm" className="text-danger" onClick={open}>
<Warning />
{formatMessage({
id: 'ReviewExamAttemptButton.exam_name',
defaultMessage: 'Second Review Required',
description: 'Table header for the table column with buttons to review exam attempts',
})}
{formatMessage(messages.ReviewExamAttemptButtonTitle)}
</Button>
</div>
<ModalDialog
Expand All @@ -37,58 +34,41 @@ const ReviewExamAttemptButton = ({
>
<ModalDialog.Header>
<ModalDialog.Title>
{formatMessage({
id: 'ReviewExamAttemptButton.confirmation_modal_title',
defaultMessage: 'Please confirm your choice.',
description: 'Title header of the modal that appears to confirm the review of an exam attempt',
})}
{formatMessage(messages.ReviewExamAttemptButtonModalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>

<ModalDialog.Body>
{formatMessage(
{
id: 'ReviewExamAttemptButton.confirmation_modal_body',
defaultMessage: `Are you sure you want to review the exam attempt for learner with username "${username}
for the exam "${examName}"?. Suspicion Level: ${suspicionLevel}.`,
description: 'Body text of the modal that appears to confirm the review of an exam attempt',
},
{ username, examName },
)}
{/* TODO: Figure out how to move this formatMessage with the variables. */}
<p>{formatMessage(messages.ReviewExamAttemptButtonModalBody)}</p>
<ul>
<li>{formatMessage(messages.Username)}{username}</li>
<li>{formatMessage(messages.ExamName)}{examName}</li>
<li>{formatMessage(messages.SuspicionLevel)}{severity}</li>
<li>{formatMessage(messages.SubmissionReason)}{submissionReason}</li>
</ul>
</ModalDialog.Body>

<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{formatMessage({
id: 'ReviewExamAttemptButton.cancel_button',
defaultMessage: 'No (Cancel)',
description: 'Text for the button to cancel reviewing an exam attempt',
})}
{formatMessage(messages.ReviewExamAttemptButtonCancel)}
</ModalDialog.CloseButton>
<Button
variant="primary"
onClick={e => { // eslint-disable-line no-unused-vars
modifyExamAttempt(attemptId, 'verify');
}}
>
{formatMessage({
id: 'ReviewExamAttemptButton.verify_button',
defaultMessage: 'Verify',
description: 'Text for the button to verify an exam attempt',
})}
{formatMessage(messages.ReviewExamAttemptButtonVerify)}
</Button>
<Button
variant="primary"
onClick={e => { // eslint-disable-line no-unused-vars
modifyExamAttempt(attemptId, 'reject');
}}
>
{formatMessage({
id: 'ReviewExamAttemptButton.reject_button',
defaultMessage: 'Reject',
description: 'Text for the button to reject an exam attempt',
})}
{formatMessage(messages.ReviewExamAttemptButtonReject)}
</Button>
</ActionRow>
</ModalDialog.Footer>
Expand All @@ -101,6 +81,8 @@ ReviewExamAttemptButton.propTypes = {
username: PropTypes.string.isRequired,
examName: PropTypes.string.isRequired,
attemptId: PropTypes.number.isRequired,
severity: PropTypes.number.isRequired,
submissionReason: PropTypes.string.isRequired,
};

export default ReviewExamAttemptButton;
Loading

0 comments on commit 92fafc4

Please sign in to comment.