From 3b7a3c4221e355c7a3503663e844dee9401b0c60 Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Wed, 27 Nov 2024 01:05:58 -0800 Subject: [PATCH 01/20] Process post request with mastery learning info --- progressReport/app.py | 56 +++++++++++++++++++++++++++++---- progressReport/requirements.txt | 1 + 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/progressReport/app.py b/progressReport/app.py index d5c857b..f7ef850 100644 --- a/progressReport/app.py +++ b/progressReport/app.py @@ -3,6 +3,7 @@ import json import os import parser +import jsonschema """ Dream Team GUI @@ -65,7 +66,30 @@ app = Flask(__name__) -@app.route('/', methods=["GET"]) +""" +Validates the fields of the mastery learning post request +""" +def validate_mastery_learning_post_request(request_as_json): + schema = { + "type": "object", + + "properties": { + # These fields can optionally be included in the POST request. + "school": {"type": "string"}, + "class": {"type": "string"}, + "class_mastery": {"type": "string"}, + }, + "patternProperties": { + "^(?!school$|class$|class_mastery).*": {"type": "integer"} # Matches mastery fields (fields other than "school", "class", and "class_mastery") + }, + "additionalProperties": False, # Disallow undefined properties + "required": [] # No fields are required + } + + # Intentionally not handling the error an improper format may produce for now. + jsonschema.validate(instance=request_as_json, schema=schema) + +@app.route('/', methods=["GET", "POST"]) def index(): def assign_node_levels(node, student_levels_count, class_levels_count): nonlocal student_mastery, class_mastery @@ -93,10 +117,31 @@ def assign_node_levels(node, student_levels_count, class_levels_count): node["class_level"] = sum(children_class_levels) // len(children_class_levels) return node["student_level"], node["class_level"] - school_name = request.args.get("school", "Berkeley") - course_name = request.args.get("class", "CS10") - student_mastery = request.args.get("student_mastery", "000000") - class_mastery = request.args.get("class_mastery", "") + # These values will be initialized in different ways depending on whether this route receives a GET or POST request + school_name = "Berkeley" + course_name = "CS10" + student_mastery = "000000" + class_mastery = "" + + # The plan can be that the POST request contains all info we currently relay to CM in the url. + if request.method == "POST": + request_as_json = request.get_json() + validate_mastery_learning_post_request(request_as_json) + school_name = request_as_json["school"] + course_name = request_as_json["class"] + class_mastery = request_as_json["class_mastery"] + + # To be consistent with the existing code, we can continue using a student_mastery string, which we can construct a student_mastery string using the content of the POST request. + student_mastery = "" + # The logic below makes use of the facts that dicts in Python are ordered and, in Flask apps, request.get_json() retains the order of the keys sent by the client. + for potential_concept, potential_mastery_level in request_as_json.items(): + if potential_concept not in ["school", "class", "class_mastery"]: + student_mastery += str(potential_mastery_level) + if request.method == "GET": + school_name = request.args.get("school", "Berkeley") + course_name = request.args.get("class", "CS10") + student_mastery = request.args.get("student_mastery", "000000") + class_mastery = request.args.get("class_mastery", "") use_url_class_mastery = True if class_mastery != "" else False if not student_mastery.isdigit(): return "URL parameter student_mastery is invalid", 400 @@ -138,6 +183,5 @@ def parse(): return "Class not found", 400 return course_data - if __name__ == '__main__': app.run() diff --git a/progressReport/requirements.txt b/progressReport/requirements.txt index 9e6cc65..efb2ac8 100644 --- a/progressReport/requirements.txt +++ b/progressReport/requirements.txt @@ -1,3 +1,4 @@ Flask gunicorn Werkzeug +jsonschema From 6fd229c43cf410d82a8934c452c6b9a9170ac131 Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Fri, 29 Nov 2024 22:31:49 -0800 Subject: [PATCH 02/20] Restore index method to original state --- progressReport/app.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/progressReport/app.py b/progressReport/app.py index f7ef850..9d79e57 100644 --- a/progressReport/app.py +++ b/progressReport/app.py @@ -89,7 +89,7 @@ def validate_mastery_learning_post_request(request_as_json): # Intentionally not handling the error an improper format may produce for now. jsonschema.validate(instance=request_as_json, schema=schema) -@app.route('/', methods=["GET", "POST"]) +@app.route('/', methods=["GET"]) def index(): def assign_node_levels(node, student_levels_count, class_levels_count): nonlocal student_mastery, class_mastery @@ -117,31 +117,10 @@ def assign_node_levels(node, student_levels_count, class_levels_count): node["class_level"] = sum(children_class_levels) // len(children_class_levels) return node["student_level"], node["class_level"] - # These values will be initialized in different ways depending on whether this route receives a GET or POST request - school_name = "Berkeley" - course_name = "CS10" - student_mastery = "000000" - class_mastery = "" - - # The plan can be that the POST request contains all info we currently relay to CM in the url. - if request.method == "POST": - request_as_json = request.get_json() - validate_mastery_learning_post_request(request_as_json) - school_name = request_as_json["school"] - course_name = request_as_json["class"] - class_mastery = request_as_json["class_mastery"] - - # To be consistent with the existing code, we can continue using a student_mastery string, which we can construct a student_mastery string using the content of the POST request. - student_mastery = "" - # The logic below makes use of the facts that dicts in Python are ordered and, in Flask apps, request.get_json() retains the order of the keys sent by the client. - for potential_concept, potential_mastery_level in request_as_json.items(): - if potential_concept not in ["school", "class", "class_mastery"]: - student_mastery += str(potential_mastery_level) - if request.method == "GET": - school_name = request.args.get("school", "Berkeley") - course_name = request.args.get("class", "CS10") - student_mastery = request.args.get("student_mastery", "000000") - class_mastery = request.args.get("class_mastery", "") + school_name = request.args.get("school", "Berkeley") + course_name = request.args.get("class", "CS10") + student_mastery = request.args.get("student_mastery", "000000") + class_mastery = request.args.get("class_mastery", "") use_url_class_mastery = True if class_mastery != "" else False if not student_mastery.isdigit(): return "URL parameter student_mastery is invalid", 400 From a8232ab502a615c202c1dd1629977740d9b1566f Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Sat, 30 Nov 2024 00:04:53 -0800 Subject: [PATCH 03/20] Generate CM from post parameters --- progressReport/app.py | 64 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/progressReport/app.py b/progressReport/app.py index 9d79e57..611e062 100644 --- a/progressReport/app.py +++ b/progressReport/app.py @@ -72,23 +72,28 @@ def validate_mastery_learning_post_request(request_as_json): schema = { "type": "object", - + # Matches mastery fields (fields other than "school" and "class" "properties": { - # These fields can optionally be included in the POST request. + # These fields are optional. "school": {"type": "string"}, "class": {"type": "string"}, - "class_mastery": {"type": "string"}, }, - "patternProperties": { - "^(?!school$|class$|class_mastery).*": {"type": "integer"} # Matches mastery fields (fields other than "school", "class", and "class_mastery") + "additionalProperties": { + "type": "object", + "properties": { + "key1": {"type": "integer"}, + "key2": {"type": "integer"} + }, + "additionalProperties": False }, - "additionalProperties": False, # Disallow undefined properties - "required": [] # No fields are required } # Intentionally not handling the error an improper format may produce for now. jsonschema.validate(instance=request_as_json, schema=schema) +""" +This method is deprecated. Query parameters are no longer used. +""" @app.route('/', methods=["GET"]) def index(): def assign_node_levels(node, student_levels_count, class_levels_count): @@ -150,6 +155,51 @@ def assign_node_levels(node, student_levels_count, class_levels_count): course_data=course_nodes) +@app.route('/', methods=["POST"]) +def generate_cm_from_post_parameters(): + request_as_json = request.get_json() + validate_mastery_learning_post_request(request_as_json) + school_name = request_as_json.get("school", "Berkeley") + course_name = request_as_json.get("class", "CS10") + def assign_node_levels(node): + if not node["children"]: + node["student_level"] = request_as_json.get(node["name"], 0) + node["class_level"] = request_as_json.get(node["name"], 0) + else: + children_student_levels = [] + children_class_levels = [] + for child in node["children"]: + student_level, class_level = assign_node_levels(child) + children_student_levels.append(student_level) + children_class_levels.append(class_level) + node["student_level"] = sum(children_student_levels) // len(children_student_levels) + node["class_level"] = sum(children_class_levels) // len(children_class_levels) + return node["student_level"], node["class_level"] + + parser.generate_map(school_name=secure_filename(school_name), course_name=secure_filename(course_name), render=True) + try: + with open("data/{}_{}.json".format(secure_filename(school_name), secure_filename(course_name))) as data_file: + course_data = json.load(data_file) + except FileNotFoundError: + return "Class not found", 400 + start_date = course_data["start date"] + course_term = course_data["term"] + class_levels = course_data["class levels"] + student_levels = course_data["student levels"] + course_node_count = course_data["count"] + course_nodes = course_data["nodes"] + assign_node_levels(course_nodes) + return render_template("web_ui.html", + start_date=start_date, + course_name=course_name, + course_term=course_term, + class_levels=class_levels, + student_levels=student_levels, + use_url_class_mastery=False, + course_node_count=course_node_count, + course_data=course_nodes) + + @app.route('/parse', methods=["POST"]) def parse(): school_name = request.args.get("school_name", "Berkeley") From 6e875a7c5d31e02f1f68cb4b308d53c331cabe55 Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Sat, 30 Nov 2024 00:29:10 -0800 Subject: [PATCH 04/20] Access nested student and class mastery keys --- progressReport/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/progressReport/app.py b/progressReport/app.py index 611e062..78d98c0 100644 --- a/progressReport/app.py +++ b/progressReport/app.py @@ -163,8 +163,8 @@ def generate_cm_from_post_parameters(): course_name = request_as_json.get("class", "CS10") def assign_node_levels(node): if not node["children"]: - node["student_level"] = request_as_json.get(node["name"], 0) - node["class_level"] = request_as_json.get(node["name"], 0) + node["student_level"] = request_as_json.get(node["name"]["student_mastery"], 0) + node["class_level"] = request_as_json.get(node["name"]["class_mastery"], 0) else: children_student_levels = [] children_class_levels = [] From d82e70af6d868c4084f43a60a2475867cb505a7f Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Mon, 2 Dec 2024 21:28:05 -0800 Subject: [PATCH 05/20] Bug fix + return correct error code --- progressReport/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/progressReport/app.py b/progressReport/app.py index 78d98c0..10b7c93 100644 --- a/progressReport/app.py +++ b/progressReport/app.py @@ -136,7 +136,7 @@ def assign_node_levels(node, student_levels_count, class_levels_count): with open("data/{}_{}.json".format(secure_filename(school_name), secure_filename(course_name))) as data_file: course_data = json.load(data_file) except FileNotFoundError: - return "Class not found", 400 + return "Class not found", 404 start_date = course_data["start date"] course_term = course_data["term"] class_levels = course_data["class levels"] @@ -163,8 +163,8 @@ def generate_cm_from_post_parameters(): course_name = request_as_json.get("class", "CS10") def assign_node_levels(node): if not node["children"]: - node["student_level"] = request_as_json.get(node["name"]["student_mastery"], 0) - node["class_level"] = request_as_json.get(node["name"]["class_mastery"], 0) + node["student_level"] = request_as_json.get(node["name"], {}).get("student_mastery", 0) + node["class_level"] = request_as_json.get(node["name"], {}).get("class_mastery", 0) else: children_student_levels = [] children_class_levels = [] @@ -181,7 +181,7 @@ def assign_node_levels(node): with open("data/{}_{}.json".format(secure_filename(school_name), secure_filename(course_name))) as data_file: course_data = json.load(data_file) except FileNotFoundError: - return "Class not found", 400 + return "Class not found", 404 start_date = course_data["start date"] course_term = course_data["term"] class_levels = course_data["class levels"] @@ -209,7 +209,7 @@ def parse(): with open("data/{}_{}.json".format(secure_filename(school_name), secure_filename(course_name))) as data_file: course_data = json.load(data_file) except FileNotFoundError: - return "Class not found", 400 + return "Class not found", 404 return course_data if __name__ == '__main__': From ed2f1c27e31c7567b913a8038e377350062112b7 Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Tue, 3 Dec 2024 15:33:08 -0800 Subject: [PATCH 06/20] Fix validation bug + load constants from file --- progressReport/app.py | 18 ++++++++++++------ progressReport/meta/defaults.json | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 progressReport/meta/defaults.json diff --git a/progressReport/app.py b/progressReport/app.py index 10b7c93..a94f391 100644 --- a/progressReport/app.py +++ b/progressReport/app.py @@ -66,6 +66,12 @@ app = Flask(__name__) +with open("meta/defaults.json", "r") as defaults_file: + default = json.load(defaults_file) + +DEFAULT_SCHOOL = default["school"] +DEFAULT_CLASS = default["class"] + """ Validates the fields of the mastery learning post request """ @@ -81,8 +87,8 @@ def validate_mastery_learning_post_request(request_as_json): "additionalProperties": { "type": "object", "properties": { - "key1": {"type": "integer"}, - "key2": {"type": "integer"} + "student_mastery": {"type": "integer"}, + "class_mastery": {"type": "integer"} }, "additionalProperties": False }, @@ -159,8 +165,8 @@ def assign_node_levels(node, student_levels_count, class_levels_count): def generate_cm_from_post_parameters(): request_as_json = request.get_json() validate_mastery_learning_post_request(request_as_json) - school_name = request_as_json.get("school", "Berkeley") - course_name = request_as_json.get("class", "CS10") + school_name = request_as_json.get("school", DEFAULT_SCHOOL) + course_name = request_as_json.get("class", DEFAULT_CLASS) def assign_node_levels(node): if not node["children"]: node["student_level"] = request_as_json.get(node["name"], {}).get("student_mastery", 0) @@ -202,8 +208,8 @@ def assign_node_levels(node): @app.route('/parse', methods=["POST"]) def parse(): - school_name = request.args.get("school_name", "Berkeley") - course_name = request.form.get("course_name", "CS10") + school_name = request.args.get("school_name", DEFAULT_SCHOOL) + course_name = request.form.get("course_name", DEFAULT_CLASS) parser.generate_map(school_name=secure_filename(school_name), course_name=secure_filename(course_name), render=False) try: with open("data/{}_{}.json".format(secure_filename(school_name), secure_filename(course_name))) as data_file: diff --git a/progressReport/meta/defaults.json b/progressReport/meta/defaults.json new file mode 100644 index 0000000..0372504 --- /dev/null +++ b/progressReport/meta/defaults.json @@ -0,0 +1 @@ +{"school": "Berkeley", "class": "CS10"} From 1e9c7c833a37c3b857658638644402445876f812 Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Wed, 11 Dec 2024 18:56:38 -0800 Subject: [PATCH 07/20] Attempt to POST CM through the iframe + transfer CM-200 changes --- progressReport/meta/Berkeley_CS10.txt | 69 ++++++++++++++++----------- progressReport/parser.py | 3 +- website/src/views/conceptMap.js | 48 +++++++++++++++++-- 3 files changed, 86 insertions(+), 34 deletions(-) diff --git a/progressReport/meta/Berkeley_CS10.txt b/progressReport/meta/Berkeley_CS10.txt index 4b89462..d67ed83 100644 --- a/progressReport/meta/Berkeley_CS10.txt +++ b/progressReport/meta/Berkeley_CS10.txt @@ -1,7 +1,7 @@ name: CS10 -term: Spring 2024 +term: Fall 2024 orientation: left to right -start date: 2024 01 15 +start date: 2024 08 26 styles: name: root, shape: ellipse, style: filled, fillcolor: #3A73A5 name: blue1, shape: ellipse, style: filled, fillcolor: #74B3CE @@ -25,33 +25,44 @@ nodes: Booleans [default, Week5] Functions [default, Week6] HOFs I [default, Week6] - Midterm No Code [blue2, Week8] - Algorithms [default, Week6] - Computers and Education [default, Week7] - Testing [default, Week7] - Programming Paradigms [default, Week8] - Computing [default, Week8] + Midterm [blue2, Week5] + Algorithms [default, Week5] + Computers and Education [default, Week5] + Testing + 2048 + Mutable/Immutable [default, Week5] + Programming Paradigms [default, Week5] + Saving the World with Computing [default, Week5] Debugging [default, Week5] Scope [default, Week5] - Iteration [default, Week5] - Recursion [default, Week8] - Complexity [default, Week7] - HOFs II [default, Week8] - Midterm Code [blue2, Week8] - Fractal [default, Week8] - Postterm No Code [blue2, Week15] - AI [default, Week14] - HCI [default, Week13] - Ethics [default, Week14] - GenAI [default, Week14] - Alumni [default, Week15] - Base Conversion [default, Week2] - Concurrency [default, Week14] - HOFs III [default, Week15] - Postterm Code [blue2, Week15] - Snap [default, Week8] - Data Stuctures [default, Week13] - Python HOFs [default, Week13] - Python OOP [default, Week13] - Tree Recursion [default, Week14] + Iteration and Randomness [default, Week5] + Recursion Tracing [default, Week5] + Algorithmic Complexity [default, Week5] + HOFs II [default, Week5] + Fractal [default, Week5] + Projects [blue2, Week5] + Project 1: Wordleâ„¢-lite [default, Week5] + Project 2: Spelling Bee [default, Week5] + Project 3: 2048 [default, Week5] + Project 4: Artifact + Documentation [default, Week5] + Project 4: Comments + Peer Feedback [default, Week5] + Project 5: Pyturis [default, Week5] + Project 6: Final Project Proposal [default, Week5] + Labs [blue2, Week5] + Lab 2: Build Your Own Blocks [default, Week2] + Lab 3: Conditionals, Reporters & Abstraction [default, Week3] + Lab 4: Lists + HOFs [default, Week3] + Lab 5: Algorithms [default, Week4] + Lab 6: Algorithmic Complexity [default, Week4] + Lab 7: Testing + 2048 [default, Week5] + Lab 8: Boards [default, Week6] + Lab 9: Trees and Fractals [default, Week6] + Lab 10: Recursive Reporters [default, Week7] + Lab 11: HOFs and Functions as Data [default, Week8] + Lab 12: Welcome to Python [default, Week9] + Lab 13: Data Structures in Python [default, Week9] + Lab 14: Linear Recursion in Python [default, Week10] + Lab 15: Tree Recursion [default, Week10] + Lab 16: Object-Oriented Programming [default, Week11] + Lab 17: Text Processing in Python [default, Week11] + Lab 18: Concurrency & Parallelism [default, Week12] + Lab 19: Data Science [default, Week13] end diff --git a/progressReport/parser.py b/progressReport/parser.py index 0a89483..c7a2ccc 100644 --- a/progressReport/parser.py +++ b/progressReport/parser.py @@ -77,7 +77,7 @@ def read_meta(f): level_match = re.search(r"\s*([A-Za-z-_\s]+): #([A-Za-z0-9]+)", line) student_levels.append({"name": level_match.group(1), "color": "#{}".format(level_match.group(2))}) elif parse_mode == "NODE": - node_match = re.search(r"(\s+)([A-Za-z0-9\-\s\\/]+) \[([A-Za-z0-9]+), Week([0-9]+)]", line) + node_match = re.search(r"(\s+)([^\[]+) \[([A-Za-z0-9]+), Week([0-9]+)]", line) root.week = max(root.week, int(node_match.group(4))) if len(node_match.group(1)) // 4 == 1: cur_node_parent = Node(node_match.group(2), node_match.group(3), node_match.group(4)) @@ -149,3 +149,4 @@ def generate_map(school_name, course_name, render=False): to_json(school_name, course_name, term, start_date, class_levels, student_levels, root, render) except FileNotFoundError: return + \ No newline at end of file diff --git a/website/src/views/conceptMap.js b/website/src/views/conceptMap.js index 564ba4c..0d1053a 100644 --- a/website/src/views/conceptMap.js +++ b/website/src/views/conceptMap.js @@ -28,10 +28,10 @@ export default function ConceptMap() { /** 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); + const {selectedStudent} = useContext(StudentSelectionContext); /** This adjusts the height of the iframe to fit its content and removes the iframe scrollbar. * This function is called when the iframe starts to load. */ @@ -69,6 +69,7 @@ export default function ConceptMap() { * changes for the instructor view. * This effect depends on the `selectedStudent` from the context. */ + /* useEffect(() => { let mounted = true; if (mounted) { @@ -82,8 +83,46 @@ export default function ConceptMap() { return () => mounted = false; }, [selectedStudent]) + */ + + /** + * Send a POST request to CM through the iframe. It uses test data for now. + */ + useEffect(() => { + console.log("iframeRef:" + Object.keys(iframeRef["current"])); + const sendPostRequest = () => { + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = `${window.location.origin}/progress`; + console.log("form.action:" + form.action); + form.target = 'ConceptMap'; + form.style.display = "none"; + + // Create a hidden input for the JSON data + const input = document.createElement("input"); + input.type = "hidden"; + input.name = "json"; + + // Just using this json data to test POST functionality + input.value = '{"Abstraction": {"student_mastery": 5, "class_mastery": 0}}'; + form.appendChild(input); + + document.body.appendChild(form); + form.submit(); + console.log("form.submit:" + JSON.stringify(form)); + document.body.removeChild(form); + + }; + + const iframe = document.getElementById("ConceptMap"); + if (iframe) { + sendPostRequest(); + } + }, []); + if (loading) { - return ; + return ; } /** @@ -99,8 +138,9 @@ 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}`} + src={`${window.location.origin}/progress`} onLoad={handleLoad} scrolling='no' allowFullScreen From a51cdc5d16c883818018fc94c546197f425b091b Mon Sep 17 00:00:00 2001 From: Naveen Sg Nathan Date: Fri, 13 Dec 2024 12:28:42 -0800 Subject: [PATCH 08/20] Implement api endpoint to return mastery mapping and post to CM in iframe --- .../students/progressquerystring/index.js | 52 ++++++++ website/src/views/conceptMap.js | 117 +++++++++--------- 2 files changed, 109 insertions(+), 60 deletions(-) diff --git a/api/v2/Routes/students/progressquerystring/index.js b/api/v2/Routes/students/progressquerystring/index.js index 657fd64..cbcdcc5 100644 --- a/api/v2/Routes/students/progressquerystring/index.js +++ b/api/v2/Routes/students/progressquerystring/index.js @@ -47,6 +47,36 @@ async function getMasteryString(userTopicPoints, maxTopicPoints) { return masteryNum; } +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}; + }); + let masteryNum = Object.values(userTopicPoints).join(''); + return masteryMapping; +} + router.get('/', async (req, res) => { const { id } = req.params; try { @@ -69,4 +99,26 @@ router.get('/', async (req, res) => { } }); +router.get('/masterymapping', 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); + 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; diff --git a/website/src/views/conceptMap.js b/website/src/views/conceptMap.js index 0d1053a..3757835 100644 --- a/website/src/views/conceptMap.js +++ b/website/src/views/conceptMap.js @@ -24,6 +24,9 @@ import apiv2 from "../utils/apiv2"; export default function ConceptMap() { const [loading, setLoading] = useState(false); const [studentMastery, setStudentMastery] = useState('000000'); + const [CMhtml, setCMhtml] = useState(''); + const [masteryMapping, setMasteryMapping] = 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 @@ -35,7 +38,7 @@ export default function ConceptMap() { /** This adjusts the height of the iframe to fit its content and removes the iframe scrollbar. * This function is called when the iframe starts to load. */ - const handleLoad = useCallback(() =>{ + const handleLoad = useCallback(() =>{ if(iframeRef.current) { const iframeDocument = iframeRef.current.contentDocument || iframeRef.current.contentWindow.document; const height = iframeDocument.body.scrollHeight; @@ -58,6 +61,9 @@ export default function ConceptMap() { setStudentMastery(res.data); setLoading(false); }); + apiv2.get(`/students/${email}/progressquerystring/masterymapping`).then((res) => { + setMasteryMapping(res.data); + }); } else { setLoading(false); } @@ -69,7 +75,7 @@ export default function ConceptMap() { * changes for the instructor view. * This effect depends on the `selectedStudent` from the context. */ - /* + useEffect(() => { let mounted = true; if (mounted) { @@ -83,69 +89,60 @@ export default function ConceptMap() { return () => mounted = false; }, [selectedStudent]) - */ - /** - * Send a POST request to CM through the iframe. It uses test data for now. - */ - useEffect(() => { - console.log("iframeRef:" + Object.keys(iframeRef["current"])); - const sendPostRequest = () => { - - const form = document.createElement('form'); - form.method = 'POST'; - form.action = `${window.location.origin}/progress`; - console.log("form.action:" + form.action); - form.target = 'ConceptMap'; - form.style.display = "none"; - - // Create a hidden input for the JSON data - const input = document.createElement("input"); - input.type = "hidden"; - input.name = "json"; - - // Just using this json data to test POST functionality - input.value = '{"Abstraction": {"student_mastery": 5, "class_mastery": 0}}'; - form.appendChild(input); - - document.body.appendChild(form); - form.submit(); - console.log("form.submit:" + JSON.stringify(form)); - document.body.removeChild(form); - - }; - - const iframe = document.getElementById("ConceptMap"); - if (iframe) { - sendPostRequest(); + + async function fetchCMHTML() { + const url = `${window.location.origin}/progress`; + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(masteryMapping) + }); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const result = await response.text(); // or response.json() if expecting JSON + return result; // Return the response + } catch (error) { + console.error("Error:", error); + return null; } - }, []); + } + + fetchCMHTML().then(result => { + setCMhtml(result); + }); + if (loading) { return ; } - /** - * Render the concept map iframe with the fetched mastery data. - * This iframe src takes in a string of numbers - * (progressQueryString) to display a concept map. - */ - return ( - <> - {/* Concept Map */} -
-