Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added ReviewExamAttemptButton #11

Merged
merged 8 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = {
Copy link
Member

@Zacharis278 Zacharis278 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry to keep nit picking on these but I think there's two different types of constants that belong in different places. One is just the statuses we get from the backend (so lower/snake case values) and the other is UI labels for those statuses. I think the former makes sense here like the ExamAttemptActions you have above but the UI labels should probably go nearer to the component that's using them and likely named accordingly. I think someone looking at this file the first time would assume these are all the backend values since it's in the data folder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the UI label mapping to AttemptsList, and kept the backend string mapping in constants.js

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
108 changes: 53 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 { 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,24 @@
/>
);

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">
{console.log('attempts:', attempts)}

Check warning on line 54 in src/pages/ExamsPage/components/AttemptList.jsx

View workflow job for this annotation

GitHub Actions / test (16)

Unexpected console statement
<DataTable
isLoading={attempts == null}
isPaginated
initialState={{
pageSize: 20,
Expand All @@ -29,58 +63,35 @@
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 +101,7 @@
})),
},
{
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 +111,14 @@
})),
},
{
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