diff --git a/src/app/components/elements/ResultsTableHeader.tsx b/src/app/components/elements/ResultsTableHeader.tsx new file mode 100644 index 0000000000..142cd7f0ad --- /dev/null +++ b/src/app/components/elements/ResultsTableHeader.tsx @@ -0,0 +1,150 @@ +import React, {Dispatch, ReactNode, SetStateAction, useContext} from "react"; +import { AssignmentProgressPageSettingsContext } from "../../../IsaacAppTypes"; +import { isAda, isPhy } from "../../services"; +import { ICON, passMark } from "./quiz/QuizProgressCommon"; +import { Label } from "reactstrap"; +import { StyledCheckbox } from "./inputs/StyledCheckbox"; +import { Spacer } from "./Spacer"; +import classNames from "classnames"; +import { CollapsibleContainer } from "./CollapsibleContainer"; +import StyledToggle from "./inputs/StyledToggle"; + +const AssignmentProgressSettings = () => { + const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); + + return
+
+ Table display mode + + assignmentProgressContext?.setFormatAsPercentage?.(e.currentTarget.checked)} + /> +
+ + {isPhy &&
+ Colour-blind mode + + assignmentProgressContext?.setColourBlind?.(e.currentTarget.checked)} + /> +
} + + {isPhy &&
+ Completion display mode + + assignmentProgressContext?.setAttemptedOrCorrect?.(e.currentTarget.checked ? "CORRECT" : "ATTEMPTED")} + /> +
} +
; +}; + +const LegendKey = ({cellClass, description}: {cellClass: string, description?: string}) => { + return
  • +
    + {description &&
    {description}
    } +
  • ; +}; + +const AssignmentProgressLegend = () => { + const context = useContext(AssignmentProgressPageSettingsContext); + const key = "key-progress-legend"; + + return
    + +
    + {context?.attemptedOrCorrect === "CORRECT" + ? + : + } +
    +
    ; +}; + + +const AdaAssignmentProgressKey = ({isAssignment}: {isAssignment?: boolean}) => { + const context = useContext(AssignmentProgressPageSettingsContext); + + const KeyItem = ({icon, label}: {icon: React.ReactNode, label: string}) => ( + + {icon} {label} + + ); + + return
    + Key + {context?.attemptedOrCorrect === "CORRECT" + ? <> +
    + + {isAssignment && } +
    +
    + + +
    + + : <> +
    + + {isAssignment && } + +
    + + } +
    ; +}; + +interface ResultsTableHeaderProps { + headerText: ReactNode; + settingsVisible: boolean; + setSettingsVisible: Dispatch>; + isAssignment?: boolean; + showLegend?: boolean; +}; + +export const ResultsTableHeader = ({headerText, settingsVisible, setSettingsVisible, isAssignment, showLegend}: ResultsTableHeaderProps) => { + const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); + + return <> +
    + {headerText} + + {isPhy && } +
    + +
    + {isPhy && + + } + + {isAda && <> + assignmentProgressContext?.setFormatAsPercentage?.(e.currentTarget.checked)} + label={Show mark as percentages} + /> + + {showLegend && } + } +
    + + {isPhy && showLegend && } + ; +}; diff --git a/src/app/components/elements/inputs/StyledCheckbox.tsx b/src/app/components/elements/inputs/StyledCheckbox.tsx index 9ff7b75fb8..5b9d9b3329 100644 --- a/src/app/components/elements/inputs/StyledCheckbox.tsx +++ b/src/app/components/elements/inputs/StyledCheckbox.tsx @@ -24,7 +24,7 @@ export const StyledCheckbox = (props: InputProps & {partial?: boolean, removeVer setChecked(props.checked ?? false); }, [props.checked]); - return
    + return
    {isAda && checked &&
    } { - const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); - - return
    -
    - Table display mode - - assignmentProgressContext?.setFormatAsPercentage?.(e.currentTarget.checked)} - /> -
    - - {isPhy &&
    - Colour-blind mode - - assignmentProgressContext?.setColourBlind?.(e.currentTarget.checked)} - /> -
    } - - {isPhy &&
    - Completion display mode - - assignmentProgressContext?.setAttemptedOrCorrect?.(e.currentTarget.checked ? "CORRECT" : "ATTEMPTED")} - /> -
    } -
    ; -}; - -export function markClassesInternal(attemptedOrCorrect: "ATTEMPTED" | "CORRECT", studentProgress: AssignmentProgressDTO, status: CompletionState | null, correctParts: number, incorrectParts: number, totalParts: number) { - if (attemptedOrCorrect === "CORRECT") { - if (!isAuthorisedFullAccess(studentProgress)) { - return "revoked"; - } else if (status === CompletionState.ALL_CORRECT || correctParts === totalParts) { - return "completed"; - } else if (status === CompletionState.NOT_ATTEMPTED || correctParts + incorrectParts === 0) { - return "not-attempted"; - } else if ((correctParts / totalParts) >= passMark) { - return "passed"; - } else if ((correctParts / totalParts) < (1 - passMark)) { - return "failed"; - } else { - return "in-progress"; - } - } else { - if (!isAuthorisedFullAccess(studentProgress)) { - return "revoked"; - } else if (status && isQuestionFullyAttempted(status) || correctParts + incorrectParts === totalParts) { - return "fully-attempted"; - } else if (status === CompletionState.NOT_ATTEMPTED || siteSpecific((correctParts + incorrectParts) / totalParts < (1 - passMark), correctParts + incorrectParts === 0)) { - return "not-attempted"; - } else if ((correctParts + incorrectParts) / totalParts >= passMark) { - return "passed"; - } else { - return "in-progress"; - } - } -} - -const BoardLink = ({id}: {id?: string}) => e.stopPropagation()}> - -; - -interface GroupAssignmentTabProps { - assignment: EnhancedAssignmentWithProgress; - progress: AssignmentProgressDTO[]; -} - -const GroupAssignmentTab = ({assignment, progress}: GroupAssignmentTabProps) => { - const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); - const questions = assignment.gameboard.contents; - - const assignmentTotalQuestionParts = questions.reduce((acc, q) => { - return acc + (q?.questionPartsTotal ?? 0); - }, 0); - - function markClasses(studentProgress: AssignmentProgressDTO, totalParts: number) { - if (!isAuthorisedFullAccess(studentProgress)) { - return "revoked"; - } - - const correctParts = studentProgress.correctQuestionPartsCount; - const incorrectParts = studentProgress.incorrectQuestionPartsCount; - const status = null; - - return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, status, correctParts, incorrectParts, totalParts); - } - - function markQuestionClasses(studentProgress: AssignmentProgressDTO, index: number) { - if (!isAuthorisedFullAccess(studentProgress)) { - return "revoked"; - } - - - const question = questions[index]; - - const totalParts = question.questionPartsTotal; - const correctParts = (studentProgress.correctPartResults || [])[index]; - const incorrectParts = (studentProgress.incorrectPartResults || [])[index]; - const status = (studentProgress.questionResults || [])[index]; - - return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, status, correctParts, incorrectParts, totalParts); - } - - const [settingsVisible, setSettingsVisible] = useState(true); - - return - -
    -
    -
    - {siteSpecific( -

    Overview: {assignment.gameboard.title}

    , -

    Group assignment overview

    - )} - See who attempted the assignment and which questions they struggled with. -
    - {isPhy && } -
    -
    - - - {isPhy && } - - assignmentId={assignment.id} progress={progress} questions={questions} - assignmentTotalQuestionParts={assignmentTotalQuestionParts} markClasses={markClasses} markQuestionClasses={markQuestionClasses} - isAssignment={true} boardId={assignment.gameboardId} - /> -
    -
    ; -}; - -export const ResultsTableHeader = ({settingsVisible, isAssignment} : {settingsVisible: boolean, isAssignment: boolean}) => { - const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); - - return <> -
    - {isPhy && -
    - -
    -
    } - - {isAda && <> - assignmentProgressContext?.setFormatAsPercentage?.(e.currentTarget.checked)} - label={Show mark as percentages} - /> - - - } -
    - ; -}; - -export const AdaAssignmentProgressKey = ({isAssignment}: {isAssignment: boolean}) => { - const context = useContext(AssignmentProgressPageSettingsContext); - - const KeyItem = ({icon, label}: {icon: React.ReactNode, label: string}) => ( - - {icon} {label} - - ); - - return
    - Key - {context?.attemptedOrCorrect === "CORRECT" - ? <> -
    - - {isAssignment && } -
    -
    - - -
    - - : <> -
    - - {isAssignment && } - -
    - - } -
    ; -}; - -const QuestionLink = ({questionId, boardId}: {questionId?: string, boardId?: string}) => e.stopPropagation()} aria-label="Open question in new tab"> - -; - -interface DetailedMarksProps extends React.HTMLAttributes { - progress: AssignmentProgressDTO[]; - questions: GameboardItem[]; - questionIndex: number; - gameboardId?: string; -} - -const DetailedMarksCard = ({progress, questions, questionIndex, gameboardId, ...rest}: DetailedMarksProps) => { - const [isOpen, setIsOpen] = useState(false); - - const difficultParts = useMemo(() => { - const totalIncorrectByPart = progress.map(p => p.questionPartResults?.[questionIndex].map(state => state === "INCORRECT" ? 1 : 0) || []).reduce((acc, curr) => { - curr.forEach((val, i) => { - acc[i] = (acc[i] || 0) + val; - }); - return acc; - }, [] as number[]); - - const total = progress.length; - return totalIncorrectByPart.reduce((acc, incorrect, index) => { - if (total >= 2 && incorrect / total >= 0.5) { - return [...acc, index]; - } - return acc; - }, [] as number[]); - }, [progress, questionIndex]); - - const numAttemptedThisQuestion = useMemo(() => { - return progress.filter(p => isQuestionFullyAttempted(p.questionResults?.[questionIndex])).length; - }, [progress, questionIndex]); - - return
    - - -
    - {/* nested divs required for clean table border when scrolling :/ */} -
    - -
    -
    -
    -
    ; -}; - -interface DetailedMarksTabProps { - assignment: EnhancedAssignmentWithProgress; - progress: AssignmentProgressDTO[]; -} - -const DetailedMarksTab = ({assignment, progress}: DetailedMarksTabProps) => { - const questions = assignment.gameboard.contents; - - return - - {siteSpecific( -

    Performance on questions

    , -

    Performance on questions

    - )} - See the questions your students answered{isPhy && " and which parts they struggled with"}. - - {isPhy &&
    - -
    } - - {questions.map((_, questionIndex) => ( - - ))} - -
    -
    ; -}; - -function isQuestionFullyAttempted (state?: CompletionState) { - return !!state && [CompletionState.ALL_CORRECT, CompletionState.ALL_ATTEMPTED, CompletionState.ALL_INCORRECT].includes(state); -} - -export const ProgressDetails = ({assignment}: { assignment: EnhancedAssignmentWithProgress }) => { - - const dispatch = useAppDispatch(); - const questions = assignment.gameboard.contents; - - const progressData = useMemo<[AssignmentProgressDTO, boolean][]>(() => assignment.progress.map(p => { - if (!isAuthorisedFullAccess(p)) return [p, false]; - - const initialState = { - ...p, - correctQuestionPagesCount: 0, - correctQuestionPartsCount: 0, - incorrectQuestionPartsCount: 0, - notAttemptedPartResults: [] - }; - - const ret = (p.questionResults || []).reduce((oldP, results, i) => { - const correctQuestionsCount = [CompletionState.ALL_CORRECT].includes(results) ? oldP.correctQuestionPagesCount + 1 : oldP.correctQuestionPagesCount; - const questions = assignment.gameboard.contents; - return { - ...oldP, - correctQuestionPagesCount: correctQuestionsCount, - correctQuestionPartsCount: oldP.correctQuestionPartsCount + (p.correctPartResults || [])[i], - incorrectQuestionPartsCount: oldP.incorrectQuestionPartsCount + (p.incorrectPartResults || [])[i], - notAttemptedPartResults: [ - ...oldP.notAttemptedPartResults, - (questions[i].questionPartsTotal - (p.correctPartResults || [])[i] - (p.incorrectPartResults || [])[i]) - ] - }; - }, initialState); - return [ret, questions.length === ret.correctQuestionPagesCount]; - }), [assignment.gameboard.contents, assignment.progress, questions.length]); - - const progress = progressData.map(pd => pd[0]); - - const numStudentsAttemptedAll = progress.filter(p => p.questionResults?.every(isQuestionFullyAttempted)).length; - const numStudentsCompletedAll = progress.filter(p => p.questionResults?.every(r => r === CompletionState.ALL_CORRECT)).length; - - return <> -
    - {isPhy && - - Back to group assignments and tests - } - {isPhy && } - -
    - - - -
    - - Due: {formatDate(assignment.dueDate)} -
    -
    - - {numStudentsAttemptedAll} of {progress.length} attempted all questions -
    -
    - - {numStudentsCompletedAll} of {progress.length} got full marks -
    -
    -
    - - - {{ - "Group overview": , - "Detailed marks": - }} - - ; - -}; diff --git a/src/app/components/pages/SingleAssignmentProgress.tsx b/src/app/components/pages/SingleAssignmentProgress.tsx deleted file mode 100644 index a2cf8923d0..0000000000 --- a/src/app/components/pages/SingleAssignmentProgress.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, {useContext} from "react"; -import {useParams} from "react-router-dom"; -import {TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; -import {Label} from "reactstrap"; -import { - useGetAssignmentProgressQuery, - useGetSingleSetAssignmentQuery -} from "../../state"; -import { - AppGroup, - AssignmentProgressPageSettingsContext, - EnhancedAssignmentWithProgress -} from "../../../IsaacAppTypes"; -import { - ASSIGNMENT_PROGRESS_CRUMB, - PATHS, - siteSpecific, - useAssignmentProgressAccessibilitySettings} from "../../services"; -import {ProgressDetails} from "./AssignmentProgressIndividual"; -import {skipToken} from "@reduxjs/toolkit/query"; -import {combineQueries, ShowLoadingQuery} from "../handlers/ShowLoadingQuery"; -import {AssignmentDTO, AssignmentProgressDTO, RegisteredUserDTO} from "../../../IsaacApiTypes"; -import { passMark } from "../elements/quiz/QuizProgressCommon"; -import { PageContainer } from "../elements/layout/PageContainer"; -import { MyAdaSidebar } from "../elements/sidebar/MyAdaSidebar"; - -const SingleProgressDetails = ({assignment}: {assignment: EnhancedAssignmentWithProgress}) => { - const pageSettings = useContext(AssignmentProgressPageSettingsContext); - - return
    - -
    ; -}; - -export const SingleAssignmentProgress = ({user, group}: {user: RegisteredUserDTO, group?: AppGroup}) => { - const params = useParams<{ assignmentId?: string }>(); - const assignmentId = parseInt(params.assignmentId || ""); // DANGER: This will produce a NaN if params.assignmentId is undefined - const assignmentQuery = useGetSingleSetAssignmentQuery(assignmentId || skipToken); - const { data: assignment } = assignmentQuery; - const assignmentProgressQuery = useGetAssignmentProgressQuery(assignmentId || skipToken); - - const groupCrumb = group && group.groupName ? {to: `${PATHS.ASSIGNMENT_PROGRESS}/group/${group.id}`, title: group.groupName} : undefined; - - const augmentAssignmentWithProgress = (assignment: AssignmentDTO, assignmentProgress: AssignmentProgressDTO[]): EnhancedAssignmentWithProgress => ({...assignment, progress: assignmentProgress} as EnhancedAssignmentWithProgress); - - const pageSettings = useAssignmentProgressAccessibilitySettings({user}); - - return - } - sidebar={siteSpecific(null, )} - > - -
    - - - -
    - } - /> -
    ; -}; - -const LegendKey = ({cellClass, description}: {cellClass: string, description?: string}) => { - return
  • -
    - {description &&
    {description}
    } -
  • ; -}; - -export const AssignmentProgressLegend = ({id}: {id?: string}) => { - const context = useContext(AssignmentProgressPageSettingsContext); - return
    - -
    - {context?.attemptedOrCorrect === "CORRECT" - ?
      - - - - - -
    - :
      - - - - -
    - } -
    -
    ; -}; diff --git a/src/app/components/pages/AssignmentProgressGroup.tsx b/src/app/components/pages/assignment_progress/AssignmentProgressGroup.tsx similarity index 93% rename from src/app/components/pages/AssignmentProgressGroup.tsx rename to src/app/components/pages/assignment_progress/AssignmentProgressGroup.tsx index ef45da4bf5..4b7368a664 100644 --- a/src/app/components/pages/AssignmentProgressGroup.tsx +++ b/src/app/components/pages/assignment_progress/AssignmentProgressGroup.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useState} from 'react'; -import {openActiveModal, useAppDispatch, useGetGroupMembersQuery, useGroupAssignments} from '../../state'; -import {AppGroup, AppQuizAssignment, AssignmentOrderSpec, EnhancedAssignment} from '../../../IsaacAppTypes'; +import {openActiveModal, useAppDispatch, useGetGroupMembersQuery, useGroupAssignments} from '../../../state'; +import {AppGroup, AppQuizAssignment, AssignmentOrderSpec, EnhancedAssignment} from '../../../../IsaacAppTypes'; import { above, AssignmentOrder, @@ -16,22 +16,22 @@ import { siteSpecific, SortOrder, useDeviceSize -} from '../../services'; -import {RegisteredUserDTO} from '../../../IsaacApiTypes'; +} from '../../../services'; +import {RegisteredUserDTO} from '../../../../IsaacApiTypes'; import {Link, useLocation} from 'react-router-dom'; -import {Spacer} from '../elements/Spacer'; -import {formatDate} from '../elements/DateString'; +import {Spacer} from '../../elements/Spacer'; +import {formatDate} from '../../elements/DateString'; import {Badge, Button, Card, CardBody, Col, Input, Label, Row} from 'reactstrap'; -import {TitleAndBreadcrumb} from '../elements/TitleAndBreadcrumb'; -import {downloadLinkModal} from '../elements/modals/AssignmentProgressModalCreators'; -import {InlineTabs} from '../elements/InlineTabs'; -import {StyledDropdown} from '../elements/inputs/DropdownInput'; -import {Loading} from '../handlers/IsaacSpinner'; +import {TitleAndBreadcrumb} from '../../elements/TitleAndBreadcrumb'; +import {downloadLinkModal} from '../../elements/modals/AssignmentProgressModalCreators'; +import {InlineTabs} from '../../elements/InlineTabs'; +import {StyledDropdown} from '../../elements/inputs/DropdownInput'; +import {Loading} from '../../handlers/IsaacSpinner'; import {skipToken} from '@reduxjs/toolkit/query'; import classNames from 'classnames'; -import { useHistoryState } from '../../state/actions/history'; -import { PageContainer } from '../elements/layout/PageContainer'; -import { MyAdaSidebar } from '../elements/sidebar/MyAdaSidebar'; +import { useHistoryState } from '../../../state/actions/history'; +import { PageContainer } from '../../elements/layout/PageContainer'; +import { MyAdaSidebar } from '../../elements/sidebar/MyAdaSidebar'; const AssignmentLikeLink = ({assignment}: {assignment: EnhancedAssignment | AppQuizAssignment}) => { const dispatch = useAppDispatch(); diff --git a/src/app/components/pages/AssignmentProgressGroupsListing.tsx b/src/app/components/pages/assignment_progress/AssignmentProgressGroupsListing.tsx similarity index 87% rename from src/app/components/pages/AssignmentProgressGroupsListing.tsx rename to src/app/components/pages/assignment_progress/AssignmentProgressGroupsListing.tsx index 614ad4b657..cb20d54c8c 100644 --- a/src/app/components/pages/AssignmentProgressGroupsListing.tsx +++ b/src/app/components/pages/assignment_progress/AssignmentProgressGroupsListing.tsx @@ -1,10 +1,10 @@ import React, {useCallback, useContext, useState} from "react"; -import {openActiveModal, useAppDispatch} from "../../state"; +import {openActiveModal, useAppDispatch} from "../../../state"; import {Card, CardBody, Col, Label, Row} from "reactstrap"; import sortBy from "lodash/sortBy"; -import {AppGroup, AssignmentProgressPageSettingsContext} from "../../../IsaacAppTypes"; -import {TitleAndBreadcrumb} from "../elements/TitleAndBreadcrumb"; -import {RegisteredUserDTO} from "../../../IsaacApiTypes"; +import {AppGroup, AssignmentProgressPageSettingsContext} from "../../../../IsaacAppTypes"; +import {TitleAndBreadcrumb} from "../../elements/TitleAndBreadcrumb"; +import {RegisteredUserDTO} from "../../../../IsaacApiTypes"; import {Link} from "react-router-dom"; import { getGroupAssignmentProgressCSVDownloadLink, @@ -14,20 +14,20 @@ import { isTeacherOrAbove, PATHS, siteSpecific -} from "../../services"; -import {downloadLinkModal} from "../elements/modals/AssignmentProgressModalCreators"; -import {PageFragment} from "../elements/PageFragment"; -import {RenderNothing} from "../elements/RenderNothing"; -import {Spacer} from "../elements/Spacer"; -import {ShowLoading} from "../handlers/ShowLoading"; -import {SearchInputWithIcon} from "../elements/SearchInputs"; -import {StyledDropdown} from "../elements/inputs/DropdownInput"; +} from "../../../services"; +import {downloadLinkModal} from "../../elements/modals/AssignmentProgressModalCreators"; +import {PageFragment} from "../../elements/PageFragment"; +import {RenderNothing} from "../../elements/RenderNothing"; +import {Spacer} from "../../elements/Spacer"; +import {ShowLoading} from "../../handlers/ShowLoading"; +import {SearchInputWithIcon} from "../../elements/SearchInputs"; +import {StyledDropdown} from "../../elements/inputs/DropdownInput"; import classNames from "classnames"; -import { PageMetadata } from "../elements/PageMetadata"; -import { PageContainer } from "../elements/layout/PageContainer"; -import { MyAdaSidebar } from "../elements/sidebar/MyAdaSidebar"; +import { PageMetadata } from "../../elements/PageMetadata"; +import { PageContainer } from "../../elements/layout/PageContainer"; +import { MyAdaSidebar } from "../../elements/sidebar/MyAdaSidebar"; -export const GroupAssignmentProgress = ({group, user}: {group: AppGroup, user: RegisteredUserDTO}) => { +const GroupAssignmentProgress = ({group, user}: {group: AppGroup, user: RegisteredUserDTO}) => { const dispatch = useAppDispatch(); const openDownloadLink = useCallback((event: React.MouseEvent) => { diff --git a/src/app/components/pages/assignment_progress/AssignmentProgressIndividual.tsx b/src/app/components/pages/assignment_progress/AssignmentProgressIndividual.tsx new file mode 100644 index 0000000000..ceeeb99de8 --- /dev/null +++ b/src/app/components/pages/assignment_progress/AssignmentProgressIndividual.tsx @@ -0,0 +1,170 @@ +import React, {useContext, useMemo, useState} from "react"; +import {Link, useParams} from "react-router-dom"; +import {TitleAndBreadcrumb} from "../../elements/TitleAndBreadcrumb"; +import { + openActiveModal, + useAppDispatch, + useGetAssignmentProgressQuery, + useGetSingleSetAssignmentQuery +} from "../../../state"; +import { + AppGroup, + AssignmentProgressPageSettingsContext, + AuthorisedAssignmentProgress, + EnhancedAssignmentWithProgress +} from "../../../../IsaacAppTypes"; +import { + ASSIGNMENT_PROGRESS_CRUMB, + getAssignmentProgressCSVDownloadLink, + isAuthorisedFullAccess, + isPhy, + PATHS, + siteSpecific, + useAssignmentProgressAccessibilitySettings} from "../../../services"; +import {skipToken} from "@reduxjs/toolkit/query"; +import {combineQueries, ShowLoadingQuery} from "../../handlers/ShowLoadingQuery"; +import {AssignmentDTO, AssignmentProgressDTO, CompletionState, RegisteredUserDTO} from "../../../../IsaacApiTypes"; +import { PageContainer } from "../../elements/layout/PageContainer"; +import { MyAdaSidebar } from "../../elements/sidebar/MyAdaSidebar"; +import classNames from "classnames"; +import { Spacer } from "../../elements/Spacer"; +import { Button, Card, CardBody } from "reactstrap"; +import { downloadLinkModal } from "../../elements/modals/AssignmentProgressModalCreators"; +import { DetailedMarksTab, GroupAssignmentTab, isQuestionFullyAttempted } from "./AssignmentProgressIndividualTabs"; +import { formatDate } from "../../elements/DateString"; +import { Tabs } from "../../elements/Tabs"; + +interface AssignmentSummaryCardProps { + studentsAttempted?: number; + studentsCompleted?: number; + totalStudents?: number; + dueDate?: number | Date; + isQuiz?: boolean; +} + +export const AssignmentSummaryCard = ({studentsAttempted, studentsCompleted, totalStudents, dueDate, isQuiz}: AssignmentSummaryCardProps) => { + return + +
    + + Due: {formatDate(dueDate)} +
    +
    + + {studentsAttempted} of {totalStudents} {isQuiz ? "submitted their test" : "attempted all questions"} +
    +
    + + {studentsCompleted} of {totalStudents} got full marks +
    +
    +
    ; +}; + +const ProgressDetails = ({assignment}: { assignment: EnhancedAssignmentWithProgress }) => { + const dispatch = useAppDispatch(); + const questions = assignment.gameboard.contents; + const pageSettings = useContext(AssignmentProgressPageSettingsContext); + + const progressData = useMemo<[AssignmentProgressDTO, boolean][]>(() => assignment.progress.map(p => { + if (!isAuthorisedFullAccess(p)) return [p, false]; + + const initialState = { + ...p, + correctQuestionPagesCount: 0, + correctQuestionPartsCount: 0, + incorrectQuestionPartsCount: 0, + notAttemptedPartResults: [] + }; + + const ret = (p.questionResults || []).reduce((oldP, results, i) => { + const correctQuestionsCount = [CompletionState.ALL_CORRECT].includes(results) ? oldP.correctQuestionPagesCount + 1 : oldP.correctQuestionPagesCount; + const questions = assignment.gameboard.contents; + return { + ...oldP, + correctQuestionPagesCount: correctQuestionsCount, + correctQuestionPartsCount: oldP.correctQuestionPartsCount + (p.correctPartResults || [])[i], + incorrectQuestionPartsCount: oldP.incorrectQuestionPartsCount + (p.incorrectPartResults || [])[i], + notAttemptedPartResults: [ + ...oldP.notAttemptedPartResults, + (questions[i].questionPartsTotal - (p.correctPartResults || [])[i] - (p.incorrectPartResults || [])[i]) + ] + }; + }, initialState); + return [ret, questions.length === ret.correctQuestionPagesCount]; + }), [assignment.gameboard.contents, assignment.progress, questions.length]); + + const progress = progressData.map(pd => pd[0]); + + const numStudentsAttemptedAll = progress.filter(p => p.questionResults?.every(isQuestionFullyAttempted)).length; + const numStudentsCompletedAll = progress.filter(p => p.questionResults?.every(r => r === CompletionState.ALL_CORRECT)).length; + const [settingsVisible, setSettingsVisible] = useState(true); + + return
    +
    + {isPhy && + + Back to group assignments and tests + } + {isPhy && } + +
    + + + + + {{ + "Group overview": , + "Detailed marks": + }} + +
    ; +}; + +export const AssignmentProgressIndividual = ({user, group}: {user: RegisteredUserDTO, group?: AppGroup}) => { + const params = useParams<{ assignmentId?: string }>(); + const assignmentId = parseInt(params.assignmentId || ""); // DANGER: This will produce a NaN if params.assignmentId is undefined + const assignmentQuery = useGetSingleSetAssignmentQuery(assignmentId || skipToken); + const { data: assignment } = assignmentQuery; + const assignmentProgressQuery = useGetAssignmentProgressQuery(assignmentId || skipToken); + + const groupCrumb = group && group.groupName ? {to: `${PATHS.ASSIGNMENT_PROGRESS}/group/${group.id}`, title: group.groupName} : undefined; + + const augmentAssignmentWithProgress = (assignment: AssignmentDTO, assignmentProgress: AssignmentProgressDTO[]): EnhancedAssignmentWithProgress => ({...assignment, progress: assignmentProgress} as EnhancedAssignmentWithProgress); + + const pageSettings = useAssignmentProgressAccessibilitySettings({user}); + + return + } + sidebar={siteSpecific(null, )} + > + +
    + + + +
    + } + /> +
    ; +}; diff --git a/src/app/components/pages/assignment_progress/AssignmentProgressIndividualTabs.tsx b/src/app/components/pages/assignment_progress/AssignmentProgressIndividualTabs.tsx new file mode 100644 index 0000000000..10b87ce6b2 --- /dev/null +++ b/src/app/components/pages/assignment_progress/AssignmentProgressIndividualTabs.tsx @@ -0,0 +1,216 @@ +import React, {Dispatch, SetStateAction, useContext, useMemo, useState} from "react"; +import { AssignmentProgressDTO, GameboardItem, CompletionState } from "../../../../IsaacApiTypes"; +import { EnhancedAssignmentWithProgress, AssignmentProgressPageSettingsContext } from "../../../../IsaacAppTypes"; +import { isAuthorisedFullAccess, isPhy, PATHS, siteSpecific } from "../../../services"; +import { passMark, ResultsTable, ResultsTablePartBreakdown } from "../../elements/quiz/QuizProgressCommon"; +import { Badge, Card, CardBody} from "reactstrap"; +import { Spacer } from "../../elements/Spacer"; +import classNames from "classnames"; +import { CollapsibleContainer } from "../../elements/CollapsibleContainer"; +import { Markup } from "../../elements/markup"; +import { ResultsTableHeader } from "../../elements/ResultsTableHeader"; + +export function markClassesInternal(attemptedOrCorrect: "ATTEMPTED" | "CORRECT", studentProgress: AssignmentProgressDTO, status: CompletionState | null, correctParts: number, incorrectParts: number, totalParts: number) { + if (attemptedOrCorrect === "CORRECT") { + if (!isAuthorisedFullAccess(studentProgress)) { + return "revoked"; + } else if (status === CompletionState.ALL_CORRECT || correctParts === totalParts) { + return "completed"; + } else if (status === CompletionState.NOT_ATTEMPTED || correctParts + incorrectParts === 0) { + return "not-attempted"; + } else if ((correctParts / totalParts) >= passMark) { + return "passed"; + } else if ((correctParts / totalParts) < (1 - passMark)) { + return "failed"; + } else { + return "in-progress"; + } + } else { + if (!isAuthorisedFullAccess(studentProgress)) { + return "revoked"; + } else if (status && isQuestionFullyAttempted(status) || correctParts + incorrectParts === totalParts) { + return "fully-attempted"; + } else if (status === CompletionState.NOT_ATTEMPTED || siteSpecific((correctParts + incorrectParts) / totalParts < (1 - passMark), correctParts + incorrectParts === 0)) { + return "not-attempted"; + } else if ((correctParts + incorrectParts) / totalParts >= passMark) { + return "passed"; + } else { + return "in-progress"; + } + } +} + +export function isQuestionFullyAttempted (state?: CompletionState) { + return !!state && [CompletionState.ALL_CORRECT, CompletionState.ALL_ATTEMPTED, CompletionState.ALL_INCORRECT].includes(state); +} + +const BoardLink = ({id}: {id?: string}) => e.stopPropagation()}> + +; + +interface GroupAssignmentTabProps { + assignment: EnhancedAssignmentWithProgress; + progress: AssignmentProgressDTO[]; + settingsVisible: boolean; + setSettingsVisible: Dispatch> +} + +export const GroupAssignmentTab = ({assignment, progress, settingsVisible, setSettingsVisible}: GroupAssignmentTabProps) => { + const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); + const questions = assignment.gameboard.contents; + + const assignmentTotalQuestionParts = questions.reduce((acc, q) => { + return acc + (q?.questionPartsTotal ?? 0); + }, 0); + + function markClasses(studentProgress: AssignmentProgressDTO, totalParts: number) { + if (!isAuthorisedFullAccess(studentProgress)) { + return "revoked"; + } + + const correctParts = studentProgress.correctQuestionPartsCount; + const incorrectParts = studentProgress.incorrectQuestionPartsCount; + const status = null; + + return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, status, correctParts, incorrectParts, totalParts); + } + + function markQuestionClasses(studentProgress: AssignmentProgressDTO, index: number) { + if (!isAuthorisedFullAccess(studentProgress)) { + return "revoked"; + } + + + const question = questions[index]; + + const totalParts = question.questionPartsTotal; + const correctParts = (studentProgress.correctPartResults || [])[index]; + const incorrectParts = (studentProgress.incorrectPartResults || [])[index]; + const status = (studentProgress.questionResults || [])[index]; + + return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, status, correctParts, incorrectParts, totalParts); + } + + return + + + {siteSpecific( +

    Overview: {assignment.gameboard.title}

    , +

    Group assignment overview

    + )} + See who attempted the assignment and which questions they struggled with. +
    } + /> + + assignmentId={assignment.id} progress={progress} questions={questions} + assignmentTotalQuestionParts={assignmentTotalQuestionParts} markClasses={markClasses} markQuestionClasses={markQuestionClasses} + isAssignment={true} boardId={assignment.gameboardId} + /> + + ; +}; + +const QuestionLink = ({questionId, boardId}: {questionId?: string, boardId?: string}) => e.stopPropagation()} aria-label="Open question in new tab"> + +; + +interface DetailedMarksProps extends React.HTMLAttributes { + progress: AssignmentProgressDTO[]; + questions: GameboardItem[]; + questionIndex: number; + gameboardId?: string; +} + +const DetailedMarksCard = ({progress, questions, questionIndex, gameboardId, ...rest}: DetailedMarksProps) => { + const [isOpen, setIsOpen] = useState(false); + + const difficultParts = useMemo(() => { + const totalIncorrectByPart = progress.map(p => p.questionPartResults?.[questionIndex].map(state => state === "INCORRECT" ? 1 : 0) || []).reduce((acc, curr) => { + curr.forEach((val, i) => { + acc[i] = (acc[i] || 0) + val; + }); + return acc; + }, [] as number[]); + + const total = progress.length; + return totalIncorrectByPart.reduce((acc, incorrect, index) => { + if (total >= 2 && incorrect / total >= 0.5) { + return [...acc, index]; + } + return acc; + }, [] as number[]); + }, [progress, questionIndex]); + + const numAttemptedThisQuestion = useMemo(() => { + return progress.filter(p => isQuestionFullyAttempted(p.questionResults?.[questionIndex])).length; + }, [progress, questionIndex]); + + return
    + + +
    + {/* nested divs required for clean table border when scrolling :/ */} +
    + +
    +
    +
    +
    ; +}; + +interface DetailedMarksTabProps { + assignment: EnhancedAssignmentWithProgress; + progress: AssignmentProgressDTO[]; + settingsVisible: boolean; + setSettingsVisible: Dispatch> +} + +export const DetailedMarksTab = ({assignment, progress, settingsVisible, setSettingsVisible}: DetailedMarksTabProps) => { + const questions = assignment.gameboard.contents; + + return + + + {siteSpecific(

    Performance on questions

    ,

    Performance on questions

    )} + See the questions your students answered{isPhy && " and which parts they struggled with"}. +
    } + /> + + {questions.map((_, questionIndex) => ( + + ))} + + + ; +}; diff --git a/src/app/components/pages/AssignmentProgressWrapper.tsx b/src/app/components/pages/assignment_progress/AssignmentProgressWrapper.tsx similarity index 89% rename from src/app/components/pages/AssignmentProgressWrapper.tsx rename to src/app/components/pages/assignment_progress/AssignmentProgressWrapper.tsx index 4c7fc7a578..3be08c908e 100644 --- a/src/app/components/pages/AssignmentProgressWrapper.tsx +++ b/src/app/components/pages/assignment_progress/AssignmentProgressWrapper.tsx @@ -1,13 +1,13 @@ import React, { useContext } from "react"; -import {useGetGroupsQuery, useGetMySetAssignmentsQuery} from "../../state"; +import {useGetGroupsQuery, useGetMySetAssignmentsQuery} from "../../../state"; import sortBy from "lodash/sortBy"; -import {RegisteredUserDTO} from "../../../IsaacApiTypes"; +import {RegisteredUserDTO} from "../../../../IsaacApiTypes"; import { useParams } from "react-router-dom"; import { AssignmentProgressGroupsListing } from "./AssignmentProgressGroupsListing"; -import { GroupSortOrder, useAssignmentProgressAccessibilitySettings } from "../../services"; +import { GroupSortOrder, useAssignmentProgressAccessibilitySettings } from "../../../services"; import { AssignmentProgressGroup } from "./AssignmentProgressGroup"; -import { AssignmentProgressPageSettingsContext } from "../../../IsaacAppTypes"; -import { SingleAssignmentProgress } from "./SingleAssignmentProgress"; +import { AssignmentProgressPageSettingsContext } from "../../../../IsaacAppTypes"; +import { AssignmentProgressIndividual } from "./AssignmentProgressIndividual"; // This exists as a wrapper around all assignment progress pages, as a way of providing the group from `getGroupsQuery` while not requesting this several times function AssignmentProgressType({user, assignmentId, groupId}: {user: RegisteredUserDTO, assignmentId?: string, groupId?: string}) { @@ -28,7 +28,7 @@ function AssignmentProgressType({user, assignmentId, groupId}: {user: Registered const assignment = assignments?.find(a => a.id === parseInt(assignmentId)); const group = groups?.find(g => g.id === assignment?.groupId); - return ; + return ; } // otherwise we are on Group Listing view diff --git a/src/app/components/pages/quizzes/QuizTeacherFeedback.tsx b/src/app/components/pages/quizzes/QuizTeacherFeedback.tsx index bbcf1d53e9..ec639dde71 100644 --- a/src/app/components/pages/quizzes/QuizTeacherFeedback.tsx +++ b/src/app/components/pages/quizzes/QuizTeacherFeedback.tsx @@ -39,7 +39,6 @@ import { Alert, Button, Card, - CardBody, Container, DropdownItem, DropdownMenu, @@ -50,33 +49,97 @@ import { import {FetchBaseQueryError} from "@reduxjs/toolkit/query"; import {SerializedError} from "@reduxjs/toolkit"; import {ShowLoadingQuery} from "../../handlers/ShowLoadingQuery"; -import {markClassesInternal, ResultsTableHeader} from "../AssignmentProgressIndividual"; +import {markClassesInternal} from "../assignment_progress/AssignmentProgressIndividualTabs"; +import {AssignmentSummaryCard} from "../assignment_progress/AssignmentProgressIndividual"; import classNames from "classnames"; import {Spacer} from "../../elements/Spacer"; +import { ResultsTableHeader } from "../../elements/ResultsTableHeader"; -const pageHelp = - See the feedback for your students for this test assignment. -; +interface QuizQuestion extends ContentBaseDTO { + questionPartsTotal?: number | undefined; +} + +const QuizProgressDetails = ({assignment}: {assignment: QuizAssignmentDTO}) => { + + const questions : QuizQuestion[] = questionsInQuiz(assignment.quiz).map(q => ({...q, questionPartsTotal: 1} as QuizQuestion)); + const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); + + function questionsInSection(section?: IsaacQuizSectionDTO) { + return section?.children?.filter(isQuestion) || []; + } + + function questionsInQuiz(quiz?: IsaacQuizDTO) { + const questions: QuizQuestion[] = []; + quiz?.children?.forEach( + section => { + questions.push(...questionsInSection(section)); + } + ); + return questions; + } + + function markClasses(studentProgress: AssignmentProgressDTO) { + if (!isAuthorisedFullAccess(studentProgress)) { + return "revoked"; + } + + const correctParts = studentProgress.correctQuestionPartsCount; + const incorrectParts = studentProgress.incorrectQuestionPartsCount; + const total = questions.reduce((acc, q) => acc + (q.questionPartsTotal ?? 0), 0); + + return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, null, correctParts, incorrectParts, total); + } + + function markQuestionClasses(studentProgress: AssignmentProgressDTO, index: number) { + if (!isAuthorisedFullAccess(studentProgress)) { + return "revoked"; + } + + const correctParts = (studentProgress.correctPartResults || [])[index]; + const incorrectParts = (studentProgress.incorrectPartResults || [])[index]; + const totalParts = questions[index].questionPartsTotal ?? 0; -const feedbackNames: Record = { - NONE: "No feedback for students", - OVERALL_MARK: "Overall mark only", - SECTION_MARKS: "Section-by-section mark breakdown", - DETAILED_FEEDBACK: "Detailed feedback on each question", + return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, null, correctParts, incorrectParts, totalParts); + } + + const totalParts = questions.length; + + const progress : AuthorisedAssignmentProgress[] = !assignment.userFeedback ? [] : assignment.userFeedback.map(user => { + const partsCorrect = questions.reduce((acc, q) => acc + (user.feedback?.questionMarks?.[q?.id ?? -1]?.correct ?? 0), 0); + return { + user: user.user as UserSummaryDTO, + completed: user.feedback?.complete ?? false, + // a list of the correct parts of an answer, one list for each question + correctPartResults: questions.map(q => user.feedback?.questionMarks?.[q?.id ?? -1]?.correct ?? 0), + incorrectPartResults: questions.map(q => user.feedback?.questionMarks?.[q?.id ?? -1]?.incorrect ?? 0), + notAttemptedPartResults: user.feedback?.complete || user.feedback?.questionMarks !== undefined + ? questions.map(q => user.feedback?.questionMarks?.[q?.id ?? -1]?.notAttempted ?? 0) + // if the quiz has not been completed (i.e. submitted), then all parts are not attempted + : questions.map(q => q.questionPartsTotal ?? 0), + questionResults: [], + correctQuestionPagesCount: partsCorrect, // quizzes don't have pages, but QuizProgressCommon expects this key to be the "Correct" column value for sorting + correctQuestionPartsCount: partsCorrect, + incorrectQuestionPartsCount: questions.reduce((acc, q) => acc + (user.feedback?.questionMarks?.[q?.id ?? -1]?.incorrect ?? 0), 0), + }; + }); + + return assignmentId={assignment.id} duedate={assignment.dueDate} progress={progress} + questions={questions} assignmentTotalQuestionParts={totalParts} markClasses={markClasses} markQuestionClasses={markQuestionClasses} + isAssignment={false}/>; }; export const QuizTeacherFeedback = ({user}: {user: RegisteredUserDTO}) => { const {quizAssignmentId} = useParams<{quizAssignmentId: string}>(); const pageSettings = useAssignmentProgressAccessibilitySettings({user}); - const numericQuizAssignmentId = parseInt(quizAssignmentId, 10); + const numericQuizAssignmentId = parseInt(quizAssignmentId ?? "", 10); const quizAssignmentQuery = useGetQuizAssignmentWithFeedbackQuery(numericQuizAssignmentId); const {data: quizAssignment} = quizAssignmentQuery; const [updateQuiz, {isLoading: isUpdatingQuiz}] = useUpdateQuizAssignmentMutation(); - const setFeedbackMode = (mode: QuizFeedbackMode) => { + const setFeedbackMode = async (mode: QuizFeedbackMode) => { if (mode !== quizAssignment?.quizFeedbackMode) { - updateQuiz({quizAssignmentId: numericQuizAssignmentId, update: {quizFeedbackMode: mode}}); + await updateQuiz({quizAssignmentId: numericQuizAssignmentId, update: {quizFeedbackMode: mode}}); } }; @@ -85,6 +148,17 @@ export const QuizTeacherFeedback = ({user}: {user: RegisteredUserDTO}) => { const quizTitle = (quizAssignment?.quiz?.title || quizAssignment?.quiz?.id || "Test"); const pageTitle = `${quizTitle} ${(assignmentNotYetStarted ? `(starts ${formatDate(assignmentStartDate)})` : "results")}`; + const pageHelp = + See the feedback for your students for this test assignment. + ; + + const feedbackNames: Record = { + NONE: "No feedback for students", + OVERALL_MARK: "Overall mark only", + SECTION_MARKS: "Section-by-section mark breakdown", + DETAILED_FEEDBACK: "Detailed feedback on each question", + }; + const buildErrorComponent = (error: FetchBaseQueryError | SerializedError | undefined) => <>

    Error loading test feedback

    @@ -144,80 +218,15 @@ export const QuizTeacherFeedback = ({user}: {user: RegisteredUserDTO}) => {
    - {/*
    -
    - - Set by: {extractTeacherName(quizAssignment.assignerSummary)} on {formatDate(quizAssignment.creationDate)} - - {quizAssignment.dueDate && - Due date: {formatDate(quizAssignment.dueDate)} - } -
    - -
    -
    - - - {feedbackNames[quizAssignment.quizFeedbackMode as QuizFeedbackMode]} - - - {QuizFeedbackModes.map(mode => - setFeedbackMode(mode)} - active={mode === quizAssignment?.quizFeedbackMode} - > - {feedbackNames[mode]} - - )} - - -
    - -
    - -
    -
    */} - - - -
    - - Due: {formatDate(quizAssignment.dueDate)} -
    -
    - - {numStudentsSubmitted} of {quizAssignment.userFeedback?.length} submitted their test -
    -
    - - {numStudentsCompletedAll} of {quizAssignment.userFeedback?.length} got full marks -
    -
    -
    +
    -
    - {siteSpecific( -

    Overview: {quizTitle}

    , -

    Group results

    - )} - - {isPhy && } -
    - - - + Overview: {quizTitle},

    Group results

    )} + />
    @@ -226,76 +235,3 @@ export const QuizTeacherFeedback = ({user}: {user: RegisteredUserDTO}) => { /> ; }; - -interface QuizQuestion extends ContentBaseDTO { - questionPartsTotal?: number | undefined; -} - -export const QuizProgressDetails = ({assignment}: {assignment: QuizAssignmentDTO}) => { - - const questions : QuizQuestion[] = questionsInQuiz(assignment.quiz).map(q => ({...q, questionPartsTotal: 1} as QuizQuestion)); - const assignmentProgressContext = useContext(AssignmentProgressPageSettingsContext); - - function questionsInSection(section?: IsaacQuizSectionDTO) { - return section?.children?.filter(isQuestion) || []; - } - - function questionsInQuiz(quiz?: IsaacQuizDTO) { - const questions: QuizQuestion[] = []; - quiz?.children?.forEach( - section => { - questions.push(...questionsInSection(section)); - } - ); - return questions; - } - - function markClasses(studentProgress: AssignmentProgressDTO) { - if (!isAuthorisedFullAccess(studentProgress)) { - return "revoked"; - } - - const correctParts = studentProgress.correctQuestionPartsCount; - const incorrectParts = studentProgress.incorrectQuestionPartsCount; - const total = questions.reduce((acc, q) => acc + (q.questionPartsTotal ?? 0), 0); - - return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, null, correctParts, incorrectParts, total); - } - - function markQuestionClasses(studentProgress: AssignmentProgressDTO, index: number) { - if (!isAuthorisedFullAccess(studentProgress)) { - return "revoked"; - } - - const correctParts = (studentProgress.correctPartResults || [])[index]; - const incorrectParts = (studentProgress.incorrectPartResults || [])[index]; - const totalParts = questions[index].questionPartsTotal ?? 0; - - return markClassesInternal(assignmentProgressContext?.attemptedOrCorrect ?? "CORRECT", studentProgress, null, correctParts, incorrectParts, totalParts); - } - - const totalParts = questions.length; - - const progress : AuthorisedAssignmentProgress[] = !assignment.userFeedback ? [] : assignment.userFeedback.map(user => { - const partsCorrect = questions.reduce((acc, q) => acc + (user.feedback?.questionMarks?.[q?.id ?? -1]?.correct ?? 0), 0); - return { - user: user.user as UserSummaryDTO, - completed: user.feedback?.complete ?? false, - // a list of the correct parts of an answer, one list for each question - correctPartResults: questions.map(q => user.feedback?.questionMarks?.[q?.id ?? -1]?.correct ?? 0), - incorrectPartResults: questions.map(q => user.feedback?.questionMarks?.[q?.id ?? -1]?.incorrect ?? 0), - notAttemptedPartResults: user.feedback?.complete || user.feedback?.questionMarks !== undefined - ? questions.map(q => user.feedback?.questionMarks?.[q?.id ?? -1]?.notAttempted ?? 0) - // if the quiz has not been completed (i.e. submitted), then all parts are not attempted - : questions.map(q => q.questionPartsTotal ?? 0), - questionResults: [], - correctQuestionPagesCount: partsCorrect, // quizzes don't have pages, but QuizProgressCommon expects this key to be the "Correct" column value for sorting - correctQuestionPartsCount: partsCorrect, - incorrectQuestionPartsCount: questions.reduce((acc, q) => acc + (user.feedback?.questionMarks?.[q?.id ?? -1]?.incorrect ?? 0), 0), - }; - }); - - return assignmentId={assignment.id} duedate={assignment.dueDate} progress={progress} - questions={questions} assignmentTotalQuestionParts={totalParts} markClasses={markClasses} markQuestionClasses={markQuestionClasses} - isAssignment={false}/>; -}; diff --git a/src/scss/phy/tabs.scss b/src/scss/phy/tabs.scss index b355ad9875..a860177714 100644 --- a/src/scss/phy/tabs.scss +++ b/src/scss/phy/tabs.scss @@ -18,3 +18,8 @@ font-size: 1.1rem; } } + +.tab-style-cards > .tab-content .card { + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/src/test/pages/AssignmentProgress.cy.tsx b/src/test/pages/AssignmentProgress.cy.tsx index 50e7c7cfc8..bffbae1286 100644 --- a/src/test/pages/AssignmentProgress.cy.tsx +++ b/src/test/pages/AssignmentProgress.cy.tsx @@ -1,6 +1,6 @@ import React from "react"; import { mockUser } from "../../mocks/data"; -import { AssignmentProgress } from "../../app/components/pages/AssignmentProgressWrapper"; +import { AssignmentProgress } from "../../app/components/pages/assignment_progress/AssignmentProgressWrapper"; import { PATHS } from "../../app/services"; import { RegisteredUserDTO } from "../../IsaacApiTypes"; diff --git a/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png index eb9093b945..0cc166adeb 100644 Binary files a/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png differ diff --git a/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png index 22819afada..d26d21aa66 100644 Binary files a/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/ada/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png differ diff --git a/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png index 168c685017..c44e46e1e7 100644 Binary files a/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Detailed Marks tab) should have no visual regressions #0.png differ diff --git a/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png b/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png index 5e4c53bc11..96e3093444 100644 Binary files a/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png and b/src/test/pages/__image_snapshots__/sci/Assignment progress Individual assignment view (Group Overview tab) should have no visual regressions #0.png differ