Skip to content

Commit

Permalink
feat: added ReviewExamAttemptButton (#11)
Browse files Browse the repository at this point in the history
* feat: added ReviewExamAttemptButton

- added call to backend api
- developing change to redux state

* feat: completed reducer, added status filter

* feat: Improved constants and messages

* fix: temp workaround for status capitalization

* fix: separated ui and backend constants

* fix: found the stray semicolon

* test: completed tests for this feat

- Fixed attempt data values to match backend constants

* fix: remove extra console log
  • Loading branch information
ilee2u authored Aug 7, 2023
1 parent 662f6f5 commit 70d1178
Show file tree
Hide file tree
Showing 19 changed files with 1,030 additions and 137 deletions.
22 changes: 22 additions & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ export const RequestKeys = {
fetchCourseExams: 'fetchCourseExams',
fetchExamAttempts: 'fetchExamAttempts',
deleteExamAttempt: 'deleteExamAttempt',
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 ErrorStatuses = {
Expand Down
84 changes: 80 additions & 4 deletions src/pages/ExamsPage/__snapshots__/index.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,35 @@ Object {
<th
colspan="1"
role="columnheader"
style="cursor: pointer;"
title="Toggle SortBy"
>
<span
class="d-flex align-items-center"
>
<span>
Time Limit
</span>
<span
class="pgn__icon"
style="opacity: 0.5;"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 10l5-5 5 5H7zM7 14l5 5 5-5H7z"
fill="currentColor"
/>
</svg>
</span>
</span>
</th>
<th
Expand Down Expand Up @@ -310,6 +332,18 @@ Object {
</span>
</span>
</th>
<th
colspan="1"
role="columnheader"
>
<span
class="d-flex align-items-center"
>
<span>
Review
</span>
</span>
</th>
</tr>
</thead>
<tbody
Expand All @@ -335,7 +369,7 @@ Object {
class="pgn__data-table-cell-wrap"
role="cell"
>
60 minutes
60
</td>
<td
class="pgn__data-table-cell-wrap"
Expand All @@ -359,7 +393,7 @@ Object {
class="pgn__data-table-cell-wrap"
role="cell"
>
Completed
Submitted
</td>
<td
class="pgn__data-table-cell-wrap"
Expand All @@ -376,6 +410,10 @@ Object {
</button>
</div>
</td>
<td
class="pgn__data-table-cell-wrap"
role="cell"
/>
</tr>
</tbody>
</table>
Expand Down Expand Up @@ -721,13 +759,35 @@ Object {
<th
colspan="1"
role="columnheader"
style="cursor: pointer;"
title="Toggle SortBy"
>
<span
class="d-flex align-items-center"
>
<span>
Time Limit
</span>
<span
class="pgn__icon"
style="opacity: 0.5;"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 10l5-5 5 5H7zM7 14l5 5 5-5H7z"
fill="currentColor"
/>
</svg>
</span>
</span>
</th>
<th
Expand Down Expand Up @@ -790,6 +850,18 @@ Object {
</span>
</span>
</th>
<th
colspan="1"
role="columnheader"
>
<span
class="d-flex align-items-center"
>
<span>
Review
</span>
</span>
</th>
</tr>
</thead>
<tbody
Expand All @@ -815,7 +887,7 @@ Object {
class="pgn__data-table-cell-wrap"
role="cell"
>
60 minutes
60
</td>
<td
class="pgn__data-table-cell-wrap"
Expand All @@ -839,7 +911,7 @@ Object {
class="pgn__data-table-cell-wrap"
role="cell"
>
Completed
Submitted
</td>
<td
class="pgn__data-table-cell-wrap"
Expand All @@ -856,6 +928,10 @@ Object {
</button>
</div>
</td>
<td
class="pgn__data-table-cell-wrap"
role="cell"
/>
</tr>
</tbody>
</table>
Expand Down
107 changes: 52 additions & 55 deletions src/pages/ExamsPage/components/AttemptList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,32 @@ import PropTypes from 'prop-types';
import { DataTable } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import ResetExamAttemptButton from './ResetExamAttemptButton';
import ReviewExamAttemptButton from './ReviewExamAttemptButton';
import messages from '../messages';

const capitalizeFirstLetter = (string) => string.charAt(0).toUpperCase() + string.slice(1);
const ExamTypes = {
proctored: 'Proctored',
timed: 'Timed',
practice: 'Practice',
onboarding: 'Onboarding',
};

const ExamAttemptStatusUILabels = {
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',
};

// This component has to be compartmentalized here otherwise npm lint throws an unstable-nested-component error.
// The button components must be compartmentalized here otherwise npm lint throws an unstable-nested-component error.
const ResetButton = (row) => (
<ResetExamAttemptButton
username={row.original.username}
Expand All @@ -14,12 +36,23 @@ const ResetButton = (row) => (
/>
);

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 AttemptList = ({ attempts }) => {
const { formatMessage, formatDate } = useIntl();

return (
<div data-testid="attempt_list">
<DataTable
isLoading={attempts == null}
isPaginated
initialState={{
pageSize: 20,
Expand All @@ -29,58 +62,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(messages.examAttemptsTableHeaderReview),
Cell: ({ row }) => (row.original.status === 'second_review_required' ? ReviewButton(row) : null),
},
]}
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),
accessor: 'time_limit',
},
{
Header: formatMessage({
id: 'AttemptsList.exam_type',
defaultMessage: 'Exam Type',
description: 'Table header for the type of the exam',
}),
Cell: ({ row }) => (capitalizeFirstLetter(row.original.exam_type)),
Header: formatMessage(messages.examAttemptsTableHeaderExamType),
Cell: ({ row }) => 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 @@ -90,11 +100,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 @@ -104,23 +110,14 @@ const AttemptList = ({ attempts }) => {
})),
},
{
Header: formatMessage({
id: 'AttemptsList.status',
defaultMessage: 'Status',
description: 'Table header for the current status of the exam attempt',
}),
Cell: ({ row }) => (capitalizeFirstLetter(row.original.status)),
Header: formatMessage(messages.examAttemptsTableHeaderStatus),
Cell: ({ row }) => ExamAttemptStatusUILabels[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
Loading

0 comments on commit 70d1178

Please sign in to comment.