Skip to content

Commit a061c5d

Browse files
committed
Fix feedback slides indexing and fetch existing
1 parent 63782bd commit a061c5d

File tree

14 files changed

+302
-43
lines changed

14 files changed

+302
-43
lines changed

backend/models/activity.mgmodel.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface TextInputActivity extends Activity {
6767
placeholder?: string;
6868
maxLength?: number;
6969
validation?: TextInputValidation;
70+
units?: string;
7071
}
7172

7273
const options2 = {
@@ -286,6 +287,10 @@ const TextInputActivitySchema = new Schema({
286287
message: "maxLength must be a positive number",
287288
},
288289
},
290+
units: {
291+
type: String,
292+
required: false,
293+
},
289294
validation: {
290295
type: {
291296
mode: {

backend/rest/feedbackRoutes.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ feedbackRouter.get(
2424
},
2525
);
2626

27+
/*
28+
Check if a learner has already submitted feedback for a module
29+
- requires moduleId and learnerId in query params
30+
- Ex. /feedbacks/check?moduleId=123&learnerId=456
31+
*/
32+
feedbackRouter.get(
33+
"/check",
34+
isAuthorizedByRole(new Set(["Learner"])),
35+
async (req, res) => {
36+
try {
37+
const { moduleId, learnerId } = req.query;
38+
if (!moduleId || !learnerId) {
39+
res.status(400).send("moduleId and learnerId are required");
40+
return;
41+
}
42+
const hasFeedback = await feedbackService.hasFeedback(
43+
learnerId as string,
44+
moduleId as string,
45+
);
46+
res.status(200).json({ hasFeedback });
47+
} catch (error) {
48+
res.status(500).send(getErrorMessage(error));
49+
}
50+
},
51+
);
52+
2753
/*
2854
Get a Feedback by its ID
2955
- requires feedbackId in request params

backend/services/implementations/activityService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ class ActivityService {
100100
],
101101
rows: 3,
102102
};
103+
} else if (questionType === QuestionType.TextInput) {
104+
activityData = {
105+
...baseActivity,
106+
validation: {
107+
mode: "short_answer",
108+
answers: [],
109+
},
110+
};
103111
} else {
104112
activityData = baseActivity;
105113
}

backend/services/implementations/feedbackService.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,23 @@ class FeedbackService implements IFeedbackService {
8888
throw error;
8989
}
9090
}
91+
92+
async hasFeedback(
93+
learnerId: string,
94+
moduleId: string,
95+
): Promise<boolean> {
96+
try {
97+
const feedback = await MgFeedback.findOne({
98+
learnerId,
99+
moduleId,
100+
});
101+
return !!feedback;
102+
} catch (error) {
103+
Logger.error(
104+
`Error checking if feedback exists: ${getErrorMessage(error)}`,
105+
);
106+
throw error;
107+
}
108+
}
91109
}
92110
export default FeedbackService;

backend/services/interfaces/feedbackService.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ interface IFeedbackService {
2222
* @throws error if feedback retrieval fails
2323
*/
2424
getAllFeedback(): Promise<FeedbackDTO[]>;
25+
26+
/**
27+
* Check if a learner has already submitted feedback for a module
28+
* @param learnerId learner's id
29+
* @param moduleId module's id
30+
* @returns true if feedback exists, false otherwise
31+
* @throws Error if check fails
32+
*/
33+
hasFeedback(learnerId: string, moduleId: string): Promise<boolean>;
2534
}
2635

2736
export default IFeedbackService;

frontend/src/APIClients/ActivityAPIClient.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,32 @@ const uploadImage = async (
130130
}
131131
};
132132

133+
const hasFeedback = async (
134+
learnerId: string,
135+
moduleId: string,
136+
): Promise<boolean> => {
137+
const bearerToken = `Bearer ${getLocalStorageObjProperty(
138+
AUTHENTICATED_USER_KEY,
139+
"accessToken",
140+
)}`;
141+
try {
142+
const { data } = await baseAPIClient.get(
143+
`/feedbacks/check?moduleId=${moduleId}&learnerId=${learnerId}`,
144+
{
145+
headers: { Authorization: bearerToken },
146+
},
147+
);
148+
return data.hasFeedback;
149+
} catch (error) {
150+
return false;
151+
}
152+
};
153+
133154
export default {
134155
createActivity,
135156
updateActivity,
136157
updateActivityMainPicture,
137158
sendFeedback,
159+
hasFeedback,
138160
uploadImage,
139161
};

frontend/src/APIClients/FeedbackAPIClient.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,21 @@ const fetchAllFeedback = async (): Promise<Array<FeedbackPopulated>> => {
1414
return data;
1515
};
1616

17-
export default { fetchAllFeedback };
17+
const hasFeedback = async (
18+
learnerId: string,
19+
moduleId: string,
20+
): Promise<boolean> => {
21+
const bearerToken = `Bearer ${getLocalStorageObjProperty(
22+
AUTHENTICATED_USER_KEY,
23+
"accessToken",
24+
)}`;
25+
const { data } = await baseAPIClient.get(
26+
`/feedbacks/check?moduleId=${moduleId}&learnerId=${learnerId}`,
27+
{
28+
headers: { Authorization: bearerToken },
29+
},
30+
);
31+
return data.hasFeedback;
32+
};
33+
34+
export default { fetchAllFeedback, hasFeedback };

frontend/src/components/course_authoring/editorComponents/PreviewLearnerModal.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import {
1313
isMultipleChoiceActivity,
1414
isMultiSelectActivity,
1515
isTableActivity,
16+
isTextInputActivity,
1617
} from "../../../types/CourseTypes";
1718
import MatchingViewer from "../../course_viewing/matching/MatchingViewer";
1819
import MultipleChoiceViewer from "../../course_viewing/multiple-choice/MultipleChoiceViewer";
1920
import TableViewer from "../../course_viewing/table/TableViewer";
21+
import TextInputViewer from "../../course_viewing/text-input/TextInputViewer";
2022

2123
const PreviewLearnerModal = ({
2224
activity,
@@ -86,6 +88,14 @@ const PreviewLearnerModal = ({
8688
isCompleted={false}
8789
/>
8890
)}
91+
{isTextInputActivity(activity) && (
92+
<TextInputViewer
93+
activity={activity}
94+
onWrongAnswer={() => {}}
95+
onCorrectAnswer={() => {}}
96+
isCompleted={false}
97+
/>
98+
)}
8999
</Box>
90100
</Dialog>
91101
);

frontend/src/components/course_authoring/text-input/TextInputEditor.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { AddPhotoAlternate } from "@mui/icons-material";
2-
import { Box, Button, Typography, useTheme } from "@mui/material";
2+
import {
3+
Box,
4+
Button,
5+
Stack,
6+
TextField,
7+
Typography,
8+
useTheme,
9+
} from "@mui/material";
310
import { VisuallyHidden } from "@reach/visually-hidden";
411
import React from "react";
512
import ActivityAPIClient from "../../../APIClients/ActivityAPIClient";
@@ -202,17 +209,22 @@ const TextInputMainEditor = ({
202209
</Box>
203210
)}
204211
</Box>
205-
<Box
206-
sx={{
207-
display: "flex",
208-
flexDirection: "row",
209-
rowGap: "24px",
210-
columnGap: "34px",
211-
flexWrap: "wrap",
212-
}}
212+
<Stack
213+
direction="row"
214+
alignItems="center"
215+
gap="24px"
216+
alignSelf="stretch"
213217
>
214-
{" "}
215-
</Box>
218+
<TextField disabled placeholder="Enter your answer here" fullWidth />
219+
{activity.units !== undefined && (
220+
<Typography
221+
variant="bodyMedium"
222+
sx={{ color: theme.palette.Neutral[600] }}
223+
>
224+
{activity.units}
225+
</Typography>
226+
)}
227+
</Stack>
216228
</Box>
217229
</Box>
218230
);

frontend/src/components/course_authoring/text-input/TextInputSidebar.tsx

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@ import {
1313
} from "@mui/material";
1414
import { Numbers, Subject, Close } from "@mui/icons-material";
1515
import IOSSwitch from "../../common/form/IOSSwitch";
16+
import {
17+
Activity,
18+
isTextInputActivity,
19+
TextInputActivity,
20+
} from "../../../types/CourseTypes";
1621

1722
interface TextInputEditorSidebarProps {
1823
mode: "short_answer" | "numeric_range";
1924
setMode: (value: "short_answer" | "numeric_range") => void;
25+
activity: TextInputActivity;
26+
setActivity: React.Dispatch<React.SetStateAction<Activity | undefined>>;
2027
hasImage: boolean;
2128
setHasImage: (value: boolean) => void;
2229
hasAdditionalContext: boolean;
@@ -25,11 +32,15 @@ interface TextInputEditorSidebarProps {
2532
setHint: (value: string) => void;
2633
correctAnswers: string[];
2734
setCorrectAnswers: (value: string[]) => void;
35+
units?: string;
36+
setUnits: (value?: string) => void;
2837
}
2938

3039
export default function TextInputEditorSidebar({
3140
mode,
3241
setMode,
42+
activity,
43+
setActivity,
3344
hasImage,
3445
setHasImage,
3546
hasAdditionalContext,
@@ -38,11 +49,12 @@ export default function TextInputEditorSidebar({
3849
setHint,
3950
correctAnswers,
4051
setCorrectAnswers,
52+
units,
53+
setUnits,
4154
}: TextInputEditorSidebarProps) {
4255
const boxHeight = "calc(100vh - 68px)";
4356
const theme = useTheme();
4457
const [currentAnswer, setCurrentAnswer] = useState("");
45-
const [hasUnits, setHasUnits] = useState(false);
4658

4759
const handleAddAnswer = (ev: React.FormEvent) => {
4860
ev.preventDefault();
@@ -237,13 +249,15 @@ export default function TextInputEditorSidebar({
237249
>
238250
<Typography variant="bodySmall">Units</Typography>
239251
<IOSSwitch
240-
checked={hasUnits}
241-
onChange={(ev) => setHasUnits(ev.target.checked)}
252+
checked={units !== undefined}
253+
onChange={(ev) => setUnits(ev.target.checked ? "" : undefined)}
242254
/>
243255
</Box>
244-
{hasUnits && (
256+
{units !== undefined && (
245257
<TextField
246258
placeholder="Unit - ex. dollars, coins, etc."
259+
defaultValue={units}
260+
onChange={(e) => setUnits(e.target.value)}
247261
sx={{ width: "100%" }}
248262
/>
249263
)}
@@ -343,8 +357,42 @@ export default function TextInputEditorSidebar({
343357
Set a number range for correct answers
344358
</Typography>
345359
<Stack direction="row" alignItems="center" gap="16px">
346-
<TextField type="number" label="Min" sx={{ width: "100%" }} />
347-
<TextField type="number" label="Max" sx={{ width: "100%" }} />
360+
<TextField
361+
type="number"
362+
label="Min"
363+
defaultValue={activity.validation.min}
364+
sx={{ width: "100%" }}
365+
onChange={(ev) =>
366+
setActivity((prev) => {
367+
if (!prev || !isTextInputActivity(prev)) return prev;
368+
return {
369+
...prev,
370+
validation: {
371+
...prev.validation,
372+
min: Number(ev.target.value),
373+
},
374+
};
375+
})
376+
}
377+
/>
378+
<TextField
379+
type="number"
380+
label="Max"
381+
defaultValue={activity.validation.max}
382+
sx={{ width: "100%" }}
383+
onChange={(ev) =>
384+
setActivity((prev) => {
385+
if (!prev || !isTextInputActivity(prev)) return prev;
386+
return {
387+
...prev,
388+
validation: {
389+
...prev.validation,
390+
max: Number(ev.target.value),
391+
},
392+
};
393+
})
394+
}
395+
/>
348396
</Stack>
349397
</>
350398
)}

0 commit comments

Comments
 (0)