-
Notifications
You must be signed in to change notification settings - Fork 0
[CM-28] Generate and process post request with mastery learning info #30
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
Changes from all commits
3b7a3c4
6fd229c
a8232ab
6e875a7
d82e70a
ed2f1c2
1e9c7c8
a51cdc5
8037a38
fcd71d8
40c97fe
99d1c45
65706c5
d454114
4d22d42
68f296c
28e54a7
564662b
307c193
ea8ae67
97b6586
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { Router } from 'express'; | ||
| import { | ||
| getMaxScores, | ||
| getStudentScores, | ||
| } from '../../../../lib/redisHelper.mjs'; | ||
| import ProgressReportData from '../../../../assets/progressReport/CS10.json' assert { type: 'json' }; | ||
Connor-Bernard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const router = Router({ mergeParams: true }); | ||
|
|
||
| function getTopicsFromUser(gradeData) { | ||
| const topicsTable = {}; | ||
| Object.entries(gradeData) | ||
| .flatMap(([assignment, topics]) => Object.entries(topics)) | ||
| .forEach(([topic, score]) => { | ||
| if (topic in topicsTable) { | ||
| topicsTable[topic] += +(score ?? 0); | ||
| } else { | ||
| topicsTable[topic] = +(score ?? 0); | ||
| } | ||
| }); | ||
| return topicsTable; | ||
| } | ||
|
|
||
| async function getMasteryMapping(userTopicPoints, maxTopicPoints) { | ||
| const numMasteryLevels = ProgressReportData['student levels'].length - 2; | ||
| Object.entries(userTopicPoints).forEach(([topic, userPoints]) => { | ||
| const maxAchievablePoints = maxTopicPoints[topic]; | ||
| if (userPoints === 0) { | ||
| return; | ||
| } | ||
| if (userPoints >= maxAchievablePoints) { | ||
| userTopicPoints[topic] = numMasteryLevels + 1; | ||
| return; | ||
| } | ||
| const unBoundedMasteryLevel = | ||
| (userPoints / maxAchievablePoints) * numMasteryLevels; | ||
| if (unBoundedMasteryLevel === numMasteryLevels) { | ||
| userTopicPoints[topic] = numMasteryLevels; | ||
| } else if (unBoundedMasteryLevel % 1 === 0) { | ||
| // Push them over to the next category if they are exactly on the edge. | ||
| userTopicPoints[topic] = unBoundedMasteryLevel + 1; | ||
| } else { | ||
| userTopicPoints[topic] = Math.ceil(unBoundedMasteryLevel); | ||
| } | ||
| }); | ||
| const masteryMapping = {}; | ||
| Object.entries(userTopicPoints).forEach(([topic, userPoints]) => { | ||
| masteryMapping[topic] = {"student_mastery": userPoints, "class_mastery": 0}; | ||
Connor-Bernard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
| return masteryMapping; | ||
| } | ||
|
|
||
| router.get('/', async (req, res) => { | ||
| const { id } = req.params; | ||
| try { | ||
| const maxScores = await getMaxScores(); | ||
| const studentScores = await getStudentScores(id); | ||
| const userTopicPoints = getTopicsFromUser(studentScores); | ||
| const maxTopicPoints = getTopicsFromUser(maxScores); | ||
| const masteryNum = await getMasteryMapping(userTopicPoints, maxTopicPoints); | ||
Connor-Bernard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return res.status(200).json(masteryNum); | ||
| } catch (err) { | ||
| switch (err.name) { | ||
| case 'StudentNotEnrolledError': | ||
| case 'KeyNotFoundError': | ||
| console.error('Error fetching student with id %s', id, err); | ||
| return res.status(404).json({ message: "Error fetching student."}); | ||
| default: | ||
| console.error('Internal service error fetching student with id %s', id, err); | ||
| return res.status(500).json({ message: "Internal server error." }); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| export default router; | ||
Connor-Bernard marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"school": "Berkeley", "class": "CS10"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| Flask | ||
| gunicorn | ||
| Werkzeug | ||
| jsonschema | ||
| deprecated |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import './css/conceptMap.css'; | |
| import jwtDecode from 'jwt-decode'; | ||
| import { StudentSelectionContext } from "../components/StudentSelectionWrapper"; | ||
| import apiv2 from "../utils/apiv2"; | ||
| import axios from "axios"; | ||
|
|
||
| /** | ||
| * The ConceptMap component renders a concept map based on student progress data from the progressQueryString API. | ||
|
|
@@ -23,12 +24,12 @@ import apiv2 from "../utils/apiv2"; | |
| */ | ||
| export default function ConceptMap() { | ||
| const [loading, setLoading] = useState(false); | ||
| const [studentMastery, setStudentMastery] = useState('000000'); | ||
| const [conceptMapHTML, setConceptMapHTML] = useState(''); | ||
|
|
||
| /** The iframeRef is initially set to null. Once the HTML webpage is loaded | ||
| * for the concept map, the iframeRef is dynamically set to the fetched | ||
| * progress report query string iframe for the selected student. | ||
| */ | ||
| */ | ||
| const iframeRef = useRef(null); | ||
|
|
||
| const { selectedStudent } = useContext(StudentSelectionContext); | ||
|
|
@@ -44,7 +45,7 @@ export default function ConceptMap() { | |
| }, []); | ||
|
|
||
| /** | ||
| * Fetch the logged-in student's mastery data on component mount (student view). | ||
| * Fetch the logged-in student's CM html on component mount (student view). | ||
| * This effect fetches data based on the JWT token stored | ||
| * in localStorage and updates the component's state. | ||
| */ | ||
|
|
@@ -53,9 +54,12 @@ export default function ConceptMap() { | |
| setLoading(true); | ||
| if (mounted && localStorage.getItem('token')) { | ||
| let email = jwtDecode(localStorage.getItem('token')).email; | ||
| // Fetch the student progressQueryString | ||
| apiv2.get(`/students/${email}/progressquerystring`).then((res) => { | ||
| setStudentMastery(res.data); | ||
| // Fetch the student masterymapping | ||
| apiv2.get(`/students/${email}/masterymapping`).then((res) => { | ||
| const conceptMapUrl = `${window.location.origin}/progress`; | ||
| axios.post(conceptMapUrl, res.data).then((res) => { | ||
| setConceptMapHTML(res.data); | ||
| }); | ||
| setLoading(false); | ||
| }); | ||
| } else { | ||
|
|
@@ -65,17 +69,20 @@ export default function ConceptMap() { | |
| }, []); | ||
|
|
||
| /** | ||
| * Fetch the selected student's mastery data whenever the selected student | ||
| * Fetch the selected student's CM html whenever the selected student | ||
| * changes for the instructor view. | ||
| * This effect depends on the `selectedStudent` from the context. | ||
| */ | ||
| useEffect(() => { | ||
| let mounted = true; | ||
| if (mounted) { | ||
| setLoading(true); | ||
| // Fetch the student progressQueryString | ||
| apiv2.get(`/students/${selectedStudent}/progressquerystring`).then((res) => { | ||
| setStudentMastery(res.data); | ||
| // Fetch the student masterymapping | ||
| apiv2.get(`/students/${selectedStudent}/masterymapping`).then((res) => { | ||
| const conceptMapUrl = `${window.location.origin}/progress`; | ||
| axios.post(conceptMapUrl, res.data).then((res) => { | ||
| setConceptMapHTML(res.data); | ||
| }); | ||
| setLoading(false); | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we set the concept map html twice?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the selected student in the dropdown changes, the html for that student’s CM is retrieved and
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh so its twice for backwards compatibility?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not quite. This code updates |
||
| } | ||
|
|
@@ -99,13 +106,14 @@ export default function ConceptMap() { | |
| ref={iframeRef} | ||
| className="concept_map_iframe" | ||
| id="ConceptMap" | ||
| name="ConceptMap" | ||
| title="Concept Map" | ||
| src={`${window.location.origin}/progress?show_legend=false&student_mastery=${studentMastery}`} | ||
| srcdoc={conceptMapHTML} | ||
| onLoad={handleLoad} | ||
| scrolling='no' | ||
| allowFullScreen | ||
| /> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.