Skip to content

Commit 0fb582a

Browse files
authored
Merge pull request #424 from isaacphysics/feature/quiz-view-as
Add option to view group member attempt at a quiz
2 parents 22b7b62 + 0414474 commit 0fb582a

File tree

13 files changed

+153
-40
lines changed

13 files changed

+153
-40
lines changed

src/IsaacApiTypes.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ export interface QuizUserFeedbackDTO {
237237
feedback?: QuizFeedbackDTO;
238238
}
239239

240+
export interface QuizAttemptFeedbackDTO {
241+
user?: UserSummaryDTO;
242+
attempt?: QuizAttemptDTO;
243+
}
244+
240245
export interface UserGameboardProgressSummaryDTO {
241246
user?: UserSummaryDTO;
242247
progress?: GameboardProgressSummaryDTO[];

src/IsaacAppTypes.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,9 @@ export type Action =
480480
| {type: ACTION_TYPE.QUIZ_START_FREE_ATTEMPT_REQUEST; quizId: string}
481481
| {type: ACTION_TYPE.QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS; attempt: ApiTypes.QuizAttemptDTO}
482482
| {type: ACTION_TYPE.QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE; error: string}
483+
| {type: ACTION_TYPE.QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_REQUEST; quizAttemptId: number; userId: number}
484+
| {type: ACTION_TYPE.QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_RESPONSE_SUCCESS; studentAttempt: ApiTypes.QuizAttemptFeedbackDTO}
485+
| {type: ACTION_TYPE.QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_RESPONSE_FAILURE; error: string}
483486

484487
| {type: ACTION_TYPE.QUIZ_ATTEMPT_MARK_COMPLETE_REQUEST; quizAttemptId: number}
485488
| {type: ACTION_TYPE.QUIZ_ATTEMPT_MARK_COMPLETE_RESPONSE_SUCCESS; attempt: ApiTypes.QuizAttemptDTO}

src/app/components/elements/quiz/QuizAttemptComponent.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import {IsaacQuizDTO, IsaacQuizSectionDTO, QuestionDTO, QuizAttemptDTO} from "../../../../IsaacApiTypes";
1+
import {
2+
IsaacQuizDTO,
3+
IsaacQuizSectionDTO,
4+
QuestionDTO,
5+
QuizAttemptDTO, UserSummaryDTO
6+
} from "../../../../IsaacApiTypes";
27
import React from "react";
38
import {isDefined} from "../../../services/miscUtils";
49
import {extractTeacherName} from "../../../services/user";
@@ -19,7 +24,7 @@ import {IsaacContentValueOrChildren} from "../../content/IsaacContentValueOrChil
1924
import {closeActiveModal, openActiveModal} from "../../../state/actions";
2025
import {UserContextPicker} from "../inputs/UserContextPicker";
2126

22-
type PageLinkCreator = (attempt: QuizAttemptDTO, page?: number) => string;
27+
type PageLinkCreator = (attempt: QuizAttemptDTO, page?: number, studentId?: string, quizAssignmentId?: string) => string;
2328

2429
export interface QuizAttemptProps {
2530
attempt: QuizAttemptDTO;
@@ -29,13 +34,16 @@ export interface QuizAttemptProps {
2934
pageLink: PageLinkCreator;
3035
pageHelp: React.ReactElement;
3136
preview?: boolean;
37+
studentId?: string;
38+
quizAssignmentId?: string;
39+
studentUser?: UserSummaryDTO;
3240
}
3341

3442
function inSection(section: IsaacQuizSectionDTO, questions: QuestionDTO[]) {
3543
return questions.filter(q => q.id?.startsWith(section.id as string + "|"));
3644
}
3745

38-
function QuizContents({attempt, sections, questions, pageLink}: QuizAttemptProps) {
46+
function QuizContents({attempt, sections, questions, pageLink, studentId, quizAssignmentId}: QuizAttemptProps) {
3947
if (isDefined(attempt.completedDate)) {
4048
return attempt.feedbackMode === "NONE" ?
4149
<h4>No feedback available</h4>
@@ -55,7 +63,7 @@ function QuizContents({attempt, sections, questions, pageLink}: QuizAttemptProps
5563
const section = sections[k];
5664
return <tr key={k}>
5765
{attempt.feedbackMode === 'DETAILED_FEEDBACK' ?
58-
<td><Link replace to={pageLink(attempt, index + 1)}>{section.title}</Link></td> :
66+
<td><Link replace to={pageLink(attempt, index + 1, studentId, quizAssignmentId)}>{section.title}</Link></td> :
5967
<td>{section.title}</td>
6068
}
6169
<td>
@@ -78,7 +86,7 @@ function QuizContents({attempt, sections, questions, pageLink}: QuizAttemptProps
7886
const answerCount = questionsInSection.filter(q => q.bestAttempt !== undefined).length;
7987
const completed = questionsInSection.length === answerCount;
8088
return <li key={k}>
81-
<Link replace to={pageLink(attempt, index + 1)}>{section.title}</Link>
89+
<Link replace to={pageLink(attempt, index + 1, studentId, quizAssignmentId)}>{section.title}</Link>
8290
{" "}
8391
<small className="text-muted">{completed ? "Completed" : anyStarted ? `${answerCount} / ${questionsInSection.length}` : ""}</small>
8492
</li>;
@@ -162,11 +170,14 @@ function QuizSection({attempt, page}: { attempt: QuizAttemptDTO, page: number })
162170

163171
export const myQuizzesCrumbs = [{title: "My tests", to: `/tests`}];
164172
export const teacherQuizzesCrumbs = [{title: "Set tests", to: `/set_tests`}];
165-
const QuizTitle = ({attempt, page, pageLink, pageHelp, preview}: QuizAttemptProps) => {
173+
const QuizTitle = ({attempt, page, pageLink, pageHelp, preview, studentId, quizAssignmentId, studentUser}: QuizAttemptProps) => {
166174
let quizTitle = attempt.quiz?.title || attempt.quiz?.id || "Test";
167175
if (isDefined(attempt.completedDate)) {
168176
quizTitle += " Feedback";
169177
}
178+
if (isDefined(studentUser)) {
179+
quizTitle += ` for ${studentUser.givenName} ${studentUser.familyName}`
180+
}
170181
if (preview) {
171182
quizTitle += " Preview";
172183
}
@@ -179,7 +190,7 @@ const QuizTitle = ({attempt, page, pageLink, pageHelp, preview}: QuizAttemptProp
179190
const section = sections && sections[page - 1] as IsaacQuizSectionDTO;
180191
const sectionTitle = section?.title ?? "Section " + page;
181192
return <TitleAndBreadcrumb currentPageTitle={sectionTitle} help={pageHelp}
182-
intermediateCrumbs={[...crumbs, {title: quizTitle, replace: true, to: pageLink(attempt)}]}/>;
193+
intermediateCrumbs={[...crumbs, {title: quizTitle, replace: true, to: pageLink(attempt, undefined, studentId, quizAssignmentId)}]}/>;
183194
}
184195
};
185196

@@ -188,12 +199,12 @@ interface QuizPaginationProps {
188199
finalLabel: string;
189200
}
190201

191-
export function QuizPagination({attempt, page, sections, pageLink, finalLabel}: QuizAttemptProps & QuizPaginationProps) {
202+
export function QuizPagination({attempt, page, sections, pageLink, finalLabel, studentId, quizAssignmentId}: QuizAttemptProps & QuizPaginationProps) {
192203
const deviceSize = useDeviceSize();
193204
const sectionCount = Object.keys(sections).length;
194-
const backLink = pageLink(attempt, page > 1 ? page - 1 : undefined);
205+
const backLink = pageLink(attempt, page > 1 ? page - 1 : undefined, studentId, quizAssignmentId);
195206
const finalSection = page === sectionCount;
196-
const nextLink = pageLink(attempt, !finalSection ? page + 1 : undefined);
207+
const nextLink = pageLink(attempt, !finalSection ? page + 1 : undefined, studentId, quizAssignmentId);
197208

198209
return <div className="d-flex w-100 justify-content-between align-items-center">
199210
<RS.Button color="primary" outline size={below["sm"](deviceSize) ? "sm" : ""} tag={Link} replace to={backLink}>Back</RS.Button>
@@ -208,7 +219,7 @@ export function QuizAttemptComponent(props: QuizAttemptProps) {
208219
<QuizTitle {...props} />
209220
{page === null ?
210221
<div className="mt-4">
211-
<QuizHeader {...props} />
222+
{!isDefined(props.studentId) && <QuizHeader {...props} />}
212223
<QuizRubric {...props}/>
213224
<QuizContents {...props} />
214225
</div>

src/app/components/navigation/IsaacApp.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,13 @@ export const IsaacApp = () => {
160160
<TrackedRoute exact path='/eventbooking/:eventId' ifUser={isLoggedIn} component={RedirectToEvent} />
161161

162162
{/* Quiz pages */}
163+
163164
<TrackedRoute exact path="/test/assignment/:quizAssignmentId" ifUser={isLoggedIn} component={QuizDoAssignment} />
164165
<TrackedRoute exact path="/test/assignment/:quizAssignmentId/page/:page" ifUser={isLoggedIn} component={QuizDoAssignment} />
165166
<TrackedRoute exact path="/test/attempt/:quizAttemptId/feedback" ifUser={isLoggedIn} component={QuizAttemptFeedback} />
166167
<TrackedRoute exact path="/test/attempt/:quizAttemptId/feedback/:page" ifUser={isLoggedIn} component={QuizAttemptFeedback} />
168+
<TrackedRoute exact path="/test/attempt/feedback/:quizAssignmentId/:studentId" ifUser={isTeacher} component={QuizAttemptFeedback} />
169+
<TrackedRoute exact path="/test/attempt/feedback/:quizAssignmentId/:studentId/:page" ifUser={isTeacher} component={QuizAttemptFeedback} />
167170
<TrackedRoute exact path="/test/assignment/:quizAssignmentId/feedback" ifUser={isTeacher} component={QuizTeacherFeedback} />
168171
<TrackedRoute exact path="/test/preview/:quizId" ifUser={isTeacher} component={QuizPreview} />
169172
<TrackedRoute exact path="/test/preview/:quizId/page/:page" ifUser={isTeacher} component={QuizPreview} />
@@ -173,6 +176,8 @@ export const IsaacApp = () => {
173176
<Redirect from="/quiz/assignment/:quizAssignmentId/feedback" to="/test/assignment/:quizAssignmentId/feedback" />
174177
<Redirect from="/quiz/assignment/:quizAssignmentId/page/:page" to="/test/assignment/:quizAssignmentId/page/:page" />
175178
<Redirect from="/quiz/assignment/:quizAssignmentId" to="/test/assignment/:quizAssignmentId" />
179+
<Redirect from="/quiz/attempt/feedback/:quizAssignmentId/:studentId/:page" to="/test/attempt/feedback/:quizAssignmentId/:studentId/:page" />
180+
<Redirect from="/quiz/attempt/feedback/:quizAssignmentId/:studentId" to="/test/attempt/feedback/:quizAssignmentId/:studentId" />
176181
<Redirect from="/quiz/attempt/:quizAttemptId/feedback/:page" to="/test/attempt/:quizAttemptId/feedback/:page" />
177182
<Redirect from="/quiz/attempt/:quizAttemptId/feedback" to="/test/attempt/:quizAttemptId/feedback" />
178183
<Redirect from="/quiz/preview/:quizId/page/:page" to="/test/preview/:quizId/page/:page" />

src/app/components/pages/quizzes/QuizAttemptFeedback.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {Link, withRouter} from "react-router-dom";
44
import * as RS from "reactstrap";
55

66
import {ShowLoading} from "../../handlers/ShowLoading";
7-
import {clearQuizAttempt, loadQuizAttemptFeedback} from "../../../state/actions/quizzes";
7+
import {
8+
clearQuizAttempt, clearStudentQuizAttempt,
9+
loadQuizAttemptFeedback,
10+
loadStudentQuizAttemptFeedback
11+
} from "../../../state/actions/quizzes";
812
import {isDefined} from "../../../services/miscUtils";
913
import {useCurrentQuizAttempt} from "../../../services/quiz";
1014
import {
@@ -19,11 +23,15 @@ import {TitleAndBreadcrumb} from "../../elements/TitleAndBreadcrumb";
1923

2024

2125
interface QuizAttemptFeedbackProps {
22-
match: {params: {quizAttemptId: string, page: string}}
26+
match: {params: {quizAttemptId?: string, page: string, studentId?: string, quizAssignmentId?: string}}
2327
}
2428

25-
const pageLink = (attempt: QuizAttemptDTO, page?: number) => {
26-
if (page !== undefined) {
29+
const pageLink = (attempt: QuizAttemptDTO, page?: number, studentId?: string, assignmentId?: string) => {
30+
if (isDefined(studentId) && isDefined(assignmentId) && page !== undefined) {
31+
return `/test/attempt/feedback/${assignmentId}/${studentId}/${page}`;
32+
} else if (isDefined(studentId) && isDefined(assignmentId)) {
33+
return `/test/attempt/feedback/${assignmentId}/${studentId}`;
34+
} else if (isDefined(page)) {
2735
return `/test/attempt/${attempt.id}/feedback/${page}`;
2836
} else {
2937
return `/test/attempt/${attempt.id}/feedback`;
@@ -32,15 +40,15 @@ const pageLink = (attempt: QuizAttemptDTO, page?: number) => {
3240

3341

3442
function QuizFooter(props: QuizAttemptProps) {
35-
const {attempt, page, pageLink} = props;
43+
const {attempt, page, pageLink, studentId, quizAssignmentId} = props;
3644

3745
let controls;
3846
let prequel = null;
3947
if (page === null) {
40-
prequel = <p className="mt-3">Click on a section title or click &lsquo;Next&rsquo; to look at your detailed feedback.</p>
48+
prequel = <p className="mt-3">Click on a section title or click &lsquo;Next&rsquo; to look at {studentId && quizAssignmentId ? "their" : "your"} detailed feedback.</p>
4149
controls = <>
4250
<Spacer/>
43-
<RS.Button tag={Link} replace to={pageLink(attempt, 1)}>Next</RS.Button>
51+
<RS.Button tag={Link} replace to={pageLink(attempt, 1, studentId, quizAssignmentId)}>Next</RS.Button>
4452
</>;
4553
} else {
4654
controls = <QuizPagination {...props} page={page} finalLabel="Back to Overview" />;
@@ -59,35 +67,45 @@ const pageHelp = <span>
5967
See the feedback for this test attempt.
6068
</span>;
6169

62-
const QuizAttemptFeedbackComponent = ({match: {params: {quizAttemptId, page}}}: QuizAttemptFeedbackProps) => {
63-
const {attempt, questions, sections, error} = useCurrentQuizAttempt();
70+
const QuizAttemptFeedbackComponent = ({match: {params: {quizAttemptId, page, studentId, quizAssignmentId}}}: QuizAttemptFeedbackProps) => {
71+
const {attempt, studentAttempt, studentUser, questions, sections, error, studentError} = useCurrentQuizAttempt();
6472

6573
const dispatch = useDispatch();
6674

6775
useEffect(() => {
68-
dispatch(loadQuizAttemptFeedback(parseInt(quizAttemptId, 10)));
76+
isDefined(quizAttemptId) && dispatch(loadQuizAttemptFeedback(parseInt(quizAttemptId, 10)));
77+
78+
if (isDefined(studentId) && isDefined(quizAssignmentId)) {
79+
dispatch(loadStudentQuizAttemptFeedback(parseInt(quizAssignmentId, 10), parseInt(studentId, 10)));
80+
}
6981

7082
return () => {
7183
dispatch(clearQuizAttempt());
84+
if (isDefined(studentId)) {
85+
dispatch(clearStudentQuizAttempt());
86+
}
7287
};
73-
}, [dispatch, quizAttemptId]);
88+
}, [dispatch, quizAttemptId, quizAssignmentId, studentId]);
89+
90+
const attemptToView = isDefined(studentId) ? studentAttempt : attempt;
91+
const errorToView = isDefined(studentId) ? studentError : error;
7492

7593
const pageNumber = isDefined(page) ? parseInt(page, 10) : null;
7694

77-
const subProps: QuizAttemptProps = {attempt: attempt as QuizAttemptDTO, page: pageNumber,
78-
questions, sections, pageLink, pageHelp};
95+
const subProps: QuizAttemptProps = {attempt: attemptToView as QuizAttemptDTO, page: pageNumber,
96+
questions, sections, pageLink, pageHelp, studentId, quizAssignmentId, studentUser};
7997

8098
return <RS.Container className="mb-5">
81-
<ShowLoading until={attempt}>
82-
{attempt && <>
99+
<ShowLoading until={attemptToView}>
100+
{isDefined(attemptToView) && <>
83101
<QuizAttemptComponent {...subProps} />
84-
{attempt.feedbackMode === 'DETAILED_FEEDBACK' && <QuizFooter {...subProps} />}
102+
{attemptToView.feedbackMode === 'DETAILED_FEEDBACK' && <QuizFooter {...subProps} />}
85103
</>}
86-
{error && <>
104+
{isDefined(errorToView) && <>
87105
<TitleAndBreadcrumb currentPageTitle="Test Feedback" intermediateCrumbs={myQuizzesCrumbs} />
88106
<RS.Alert color="danger">
89107
<h4 className="alert-heading">Error loading your feedback!</h4>
90-
<p>{error}</p>
108+
<p>{errorToView}</p>
91109
</RS.Alert>
92110
</>}
93111
</ShowLoading>

src/app/components/pages/quizzes/QuizTeacherFeedback.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ interface ResultRowProps {
7979
assignment: QuizAssignmentDTO;
8080
}
8181

82+
function openStudentFeedback(assignment: QuizAssignmentDTO, userId: number | undefined) {
83+
isDefined(assignment?.id) && isDefined(userId) && window.open(`/quiz/attempt/feedback/${assignment.id}/${userId}`, '_blank')
84+
}
85+
8286
function ResultRow({pageSettings, row, assignment}: ResultRowProps) {
8387
const [dropdownOpen, setDropdownOpen] = useState(false);
8488
const [working, setWorking] = useState(false);
@@ -101,7 +105,7 @@ function ResultRow({pageSettings, row, assignment}: ResultRowProps) {
101105
Confirm
102106
</RS.Button>,
103107
]
104-
}));
108+
}));
105109
}
106110

107111
const _returnToStudent = async () => {
@@ -123,7 +127,7 @@ function ResultRow({pageSettings, row, assignment}: ResultRowProps) {
123127
} else if (!row.feedback?.complete) {
124128
message = "Not completed";
125129
}
126-
const valid = message === undefined;
130+
const valid = !isDefined(message);
127131
return <tr className={`${row.user?.authorisedFullAccess ? "" : " not-authorised"}`} title={`${row.user?.givenName + " " + row.user?.familyName}`}>
128132
<th className="student-name">
129133
{valid ?
@@ -169,7 +173,7 @@ function ResultRow({pageSettings, row, assignment}: ResultRowProps) {
169173
}).flat()
170174
})}
171175
<td className="total-column">
172-
{formatMark(row.feedback?.overallMark?.correct as number, quiz?.total as number, pageSettings.formatAsPercentage)}
176+
<RS.Button size="sm" onClick={() => openStudentFeedback(assignment, row.user?.id)}>{formatMark(row.feedback?.overallMark?.correct as number, quiz?.total as number, pageSettings.formatAsPercentage)}</RS.Button>
173177
</td>
174178
</>}
175179
</tr>;
@@ -295,7 +299,7 @@ const QuizTeacherFeedbackComponent = ({match: {params: {quizAssignmentId}}}: Qui
295299
</div>
296300
</RS.Col>}
297301
<RS.Col>
298-
<RS.Label for="feedbackMode" className="pr-1">Feedback mode:</RS.Label><br/>
302+
<RS.Label for="feedbackMode" className="pr-1">Student feedback mode:</RS.Label><br/>
299303
<RS.UncontrolledDropdown className="d-inline-block">
300304
<RS.DropdownToggle color="dark" outline className={"px-3 text-nowrap"} caret={!settingFeedbackMode} id="feedbackMode" disabled={settingFeedbackMode}>
301305
{settingFeedbackMode ?

src/app/services/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,9 @@ export const api = {
573573
loadQuizAttemptFeedback: (quizAttemptId: number): AxiosPromise<ApiTypes.QuizAttemptDTO> => {
574574
return endpoint.get(`/quiz/attempt/${quizAttemptId}/feedback`);
575575
},
576+
loadStudentQuizAttemptFeedback: (quizAssignmentId: number, userId: number): AxiosPromise<ApiTypes.QuizAttemptFeedbackDTO> => {
577+
return endpoint.get(`/quiz/assignment/${quizAssignmentId}/attempt/${userId}`)
578+
},
576579
loadQuizAssignmentFeedback: (quizAssignmentId: number): AxiosPromise<ApiTypes.QuizAssignmentDTO> => {
577580
return endpoint.get(`/quiz/assignment/${quizAssignmentId}`);
578581
},

src/app/services/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,9 @@ export enum ACTION_TYPE {
509509
QUIZ_START_FREE_ATTEMPT_REQUEST = "QUIZ_START_FREE_ATTEMPT_REQUEST",
510510
QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS = "QUIZ_LOAD_ATTEMPT_RESPONSE_SUCCESS",
511511
QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE = "QUIZ_LOAD_ATTEMPT_RESPONSE_FAILURE",
512+
QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_REQUEST = "QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_REQUEST",
513+
QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_RESPONSE_SUCCESS = "QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_RESPONSE_SUCCESS",
514+
QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_RESPONSE_FAILURE = "QUIZ_LOAD_STUDENT_ATTEMPT_FEEDBACK_RESPONSE_FAILURE",
512515

513516
QUIZ_ATTEMPT_MARK_COMPLETE_REQUEST = "QUIZ_ATTEMPT_MARK_COMPLETE_REQUEST",
514517
QUIZ_ATTEMPT_MARK_COMPLETE_RESPONSE_SUCCESS = "QUIZ_ATTEMPT_MARK_COMPLETE_RESPONSE_SUCCESS",

0 commit comments

Comments
 (0)