From fa870483e924633705040a4acd1f89f3cfdcf696 Mon Sep 17 00:00:00 2001 From: Jason Altekruse Date: Wed, 10 Jan 2024 13:04:50 -0600 Subject: [PATCH] Squashed commit of the following: commit 393831d511f313bbdc7fd2d036df0287018478b5 Author: Jason Altekruse Date: Wed Jan 10 13:03:50 2024 -0600 Cleanup, would like to turn some of the logging changes in a separate PR commit 2c2baf1b2684920ce346f2c5b1f5208343e9e86c Author: Jason Altekruse Date: Wed Jan 10 11:45:30 2024 -0600 Squashed commit of the following: commit 429bd06427aa313061a0af3fa7da0b685b6f59e8 Author: Jason Altekruse Date: Tue Jan 9 15:37:12 2024 -0600 Remove big files - being pulled in from temporary CDN during review, will decide on final location during discussion in PR review commit 30a72ed70a963433d1667538d2117a1478283016 Author: Jason Altekruse Date: Sun Jan 7 21:37:01 2024 -0600 point to GH pages temporarily until we get a real release of DoenetML up on NPM commit c7091d0be5122840f915051e18564946adb2774b Author: Jason Altekruse Date: Mon Dec 4 14:49:41 2023 -0600 Give a valid response if no student page state is stored yet commit d33ed6d192fdafb4c89fbdb619fdd1d53056c57e Author: Jason Altekruse Date: Mon Nov 27 23:17:30 2023 -0600 recalling state works! commit 3b35d6bff3ab922d765481ea3a1fd9aa32f75fbf Author: Jason Altekruse Date: Mon Nov 27 22:19:10 2023 -0600 TODO - come back to this state and figure out why a 500 server error ends up with a useless payload of {"detail":{}} WIP - trying to get state loading from the server commit a9200529270ae439b19740ff25638bf9f4fa3431 Merge: 2eabe19 7d76814 Author: Jason Altekruse Date: Wed Nov 22 11:21:28 2023 -0600 Merge remote-tracking branch 'origin/main' into doenet-question-type Conflicts: bases/rsptx/web2py_server/applications/runestone/static/js/admin.js poetry.lock projects/interactives/poetry.lock projects/interactives/pyproject.toml projects/w2p_login_assign_grade/poetry.lock projects/w2p_login_assign_grade/pyproject.toml pyproject.toml commit 2eabe19a1b08831724f635b5d2c3813dcaaeb327 Author: Jason Altekruse Date: Wed Nov 22 11:12:43 2023 -0600 Debugging inconsistent experience having grades calculated as a student - last time I tried hunting this down the dockerized version was behaving different than the dstarted version of the server commit 9ae91b18fc0887341efbfbb45255e7b3e489d356 Author: Jason Altekruse Date: Wed Nov 22 11:11:54 2023 -0600 WIP - some pointers to where file uploads are currently done by students, should be able to be repurposed to allow image uploads for doenet content commit 4e159ff813313b04cde7b577ff7838427f5bab9f Author: Jason Altekruse Date: Wed Nov 22 11:04:57 2023 -0600 Integrate the 2 panel doenet editor commit 3a91363e0851f1d937182c65d4fb65c06a774045 Author: Jason Altekruse Date: Tue Nov 7 12:51:17 2023 -0600 Auto-grading for Doenet questions TODO - remove the copy-paste in this file Discuss possibly going as far as consolidating all of the answer tables into 1, I believe that would remove the need for several different mappings and "specialized" copy/pasted functions for each question type that I have needed to find commit c86b5bc613934ef048cc41d8f41ec54243919134 Author: Jason Altekruse Date: Mon Nov 6 08:56:09 2023 -0600 WIP - trying to get grades saving and showing up in the gradebook for doenet problems commit e4d9155e709da6184fc3dd831df747767c523727 Author: Jason Altekruse Date: Tue Oct 24 22:07:44 2023 -0500 add useful info to logging of errors commit ef97f0d5cf603fb0355ad23e109cef5dcf44064a Author: Jason Altekruse Date: Tue Oct 24 22:06:57 2023 -0500 poetry locks commit 0d5771f482150d639de3d863502890af1fbca538 Author: Jason Altekruse Date: Tue Oct 24 22:06:25 2023 -0500 WIP - adding doenet to the dropdown, and getting it to show up in the table after being added to an assignment commit c14b3f68b98cacd1068affe1aefb2d86e1715f87 Author: Jason Altekruse Date: Tue Oct 24 22:04:08 2023 -0500 fixing other question types hopefully with codeChat downgrade commit f1c29dd8720fa726fd0a46c2c26770300d821738 Author: Jason Altekruse Date: Tue Oct 24 18:01:04 2023 -0500 making a request to backend! Next I need to figure out how I get access to the minimal necessary data for the request, mostly the course name from schemas.py # Schemas # ======= class LogItemIncoming(BaseModelNone): """ This class defines the schema for what we can expect to get from a logging event. Because we are using pydantic type verification happens automatically, if we want to add additional constraints we can do so. """ event: str act: str div_id: str course_name: str commit ba60d59298aaedc6737a1e33653f4a69413232c7 Author: Jason Altekruse Date: Tue Oct 24 16:37:24 2023 -0500 I think this is the minimal set of changes to get Doenet rendering - making one change over in DoenetML right now to allow passing more config options (redoing change from before I started from the tip of DoenetML recently) commit 0c7629025c7a3115692b9d29e21367f42165fcfc Author: Jason Altekruse Date: Thu Oct 19 18:43:07 2023 -0500 I am confused, but now it lets me update the JS with just building the NPM project and re-building the book? commit 789824ae185609f834cd8ef54472950c5bf0da26 Author: Jason Altekruse Date: Thu Oct 19 18:42:36 2023 -0500 Getting doenet into the standard initialization path for runestone components - TODO - refactor doenet-embed to produce an artifact that is importable here, may have a good format with a module and instead need to update the config on this side, this was the error I got // TODO fix this, in the meantime including from sphinx_static_files.html // ERROR in ./runestone/doenet/js/doenet-standalone.js 240673:19 //Module parse failed: Unterminated template (240673:19) //You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders commit 142aaf13ad74f0b9c2257a278ec483ef2d381eb1 Author: Jason Altekruse Date: Mon Oct 16 17:15:24 2023 -0500 lock files, I still can't get it pulling in the old version of codeChat commit d91f08492dddf9727b641f4851a6f170d333a0ca Author: Jason Altekruse Date: Mon Oct 16 17:14:59 2023 -0500 WIP - got doenet showing in the assignment editor --- .../book_server_api/routers/assessment.py | 62 ++++++ .../book_server_api/routers/rslogging.py | 2 +- .../rsptx/interactives/runestone/__init__.py | 3 + .../interactives/runestone/doenet/__init__.py | 1 + .../interactives/runestone/doenet/doenet.py | 181 ++++++++++++++++++ .../runestone/doenet/js/doenet.js | 141 ++++++++++++++ bases/rsptx/interactives/webpack.index.js | 2 + .../runestone/controllers/admin.py | 2 + .../applications/runestone/models/db_ebook.py | 12 ++ .../runestone/modules/questions_report.py | 19 ++ .../runestone/modules/rs_grading.py | 49 +++++ .../applications/runestone/static/js/admin.js | 61 +++++- .../runestone/views/_sphinx_static_files.html | 3 + .../runestone/views/admin/assignments.html | 25 ++- components/rsptx/db/crud.py | 1 + components/rsptx/db/models.py | 7 + components/rsptx/logging/applogger.py | 2 +- components/rsptx/templates/_base.html | 3 + .../rsptx/templates/staticAssets/js/admin.js | 2 +- 19 files changed, 565 insertions(+), 13 deletions(-) create mode 100755 bases/rsptx/interactives/runestone/doenet/__init__.py create mode 100644 bases/rsptx/interactives/runestone/doenet/doenet.py create mode 100644 bases/rsptx/interactives/runestone/doenet/js/doenet.js diff --git a/bases/rsptx/book_server_api/routers/assessment.py b/bases/rsptx/book_server_api/routers/assessment.py index 7f426d71..6c54299b 100644 --- a/bases/rsptx/book_server_api/routers/assessment.py +++ b/bases/rsptx/book_server_api/routers/assessment.py @@ -23,6 +23,8 @@ # ------------------- from bleach import clean from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder from pydantic import BaseModel # Local application imports @@ -67,6 +69,66 @@ tags=["assessment"], ) +@router.get("/getDoenetState") +async def getdoenetstate(request: Request, div_id: str, + course_name: str, event: str, + # sid: Optional[str], + user=Depends(auth_manager) +): + request_data = AssessmentRequest(course=course_name, div_id=div_id, event=event) + # if the user is not logged in an HTTP 401 will be returned. + # Otherwise if the user is an instructor then use the provided + # sid (it could be any student in the class). If none is provided then + # use the user objects username + sid = user.username + if await is_instructor(request): + if request_data.sid: + sid = request_data.sid + else: + if request_data.sid: + # someone is attempting to spoof the api + return make_json_response( + status=status.HTTP_401_UNAUTHORIZED, detail="not an instructor" + ) + request_data.sid = sid + + + row = await fetch_last_answer_table_entry(request_data) + # mypy complains that ``row.id`` doesn't exist (true, but the return type wasn't exact and this does exist). + if not row or row.id is None: # type: ignore + return JSONResponse( + status_code=200, content=jsonable_encoder({"loadedState": False, "success": True}) + ) + ret = row.dict() + rslogger.debug(f"row is {ret}") + if "timestamp" in ret: + ret["timestamp"] = ( + ret["timestamp"].replace(tzinfo=datetime.timezone.utc).isoformat() + ) + rslogger.debug(f"timestamp is {ret['timestamp']}") + + # Do server-side grading if needed, which restores the answer and feedback. + if feedback := await is_server_feedback(request_data.div_id, request_data.course): + rcd = runestone_component_dict[EVENT2TABLE[request_data.event]] + # The grader should also be defined if there's feedback. + assert rcd.grader + # Use the grader to add server-side feedback to the returned dict. + ret.update(await rcd.grader(row, feedback)) + + # get grade and instructor feedback if Any + grades = await fetch_question_grade(sid, request_data.course, request_data.div_id) + if grades: + ret["comment"] = grades.comment + ret["score"] = grades.score + + real_ret = ret["answer"]["state"] + real_ret["success"] = True + real_ret["loadedState"] = True + rslogger.debug(f"Returning {ret}") + # return make_json_response(detail=ret) + return JSONResponse( + status_code=200, content=jsonable_encoder(real_ret) + ) # getAssessResults # ---------------- diff --git a/bases/rsptx/book_server_api/routers/rslogging.py b/bases/rsptx/book_server_api/routers/rslogging.py index 7414f90a..93048564 100644 --- a/bases/rsptx/book_server_api/routers/rslogging.py +++ b/bases/rsptx/book_server_api/routers/rslogging.py @@ -149,7 +149,7 @@ async def log_book_event( if entry.act in ["start", "pause", "resume"]: # We don't need these in the answer table but want the event to be timedExam. create_answer_table = False - elif entry.event == "webwork" or entry.event == "hparsonsAnswer": + elif entry.event == "webwork" or entry.event == "hparsonsAnswer" or entry.event == "doenet": entry.answer = json.loads(useinfo_dict["answer"]) if create_answer_table: diff --git a/bases/rsptx/interactives/runestone/__init__.py b/bases/rsptx/interactives/runestone/__init__.py index 0b544cac..27b6afb6 100644 --- a/bases/rsptx/interactives/runestone/__init__.py +++ b/bases/rsptx/interactives/runestone/__init__.py @@ -11,6 +11,7 @@ from .datafile import DataFile from .disqus import DisqusDirective from .dragndrop import DragNDrop +from .doenet import DoenetDirective from .fitb import FillInTheBlank from .groupsub import GroupSubmission from .hparsons import HParsonsDirective @@ -40,6 +41,7 @@ # TODO: clean up - many of the folders are not needed as the files are imported by webpack +# TODO - Jason second's this TODO, I've been confused by duplicates copies of static assets # # runestone_static_dirs() # ----------------------- @@ -251,6 +253,7 @@ def build(options): "datafile": DataFile, "disqus": DisqusDirective, "dragndrop": DragNDrop, + "doenet": DoenetDirective, "groupsub": GroupSubmission, "hparsons": HParsonsDirective, "parsonsprob": ParsonsProblem, diff --git a/bases/rsptx/interactives/runestone/doenet/__init__.py b/bases/rsptx/interactives/runestone/doenet/__init__.py new file mode 100755 index 00000000..14312333 --- /dev/null +++ b/bases/rsptx/interactives/runestone/doenet/__init__.py @@ -0,0 +1 @@ +from .doenet import * diff --git a/bases/rsptx/interactives/runestone/doenet/doenet.py b/bases/rsptx/interactives/runestone/doenet/doenet.py new file mode 100644 index 00000000..8375a2d0 --- /dev/null +++ b/bases/rsptx/interactives/runestone/doenet/doenet.py @@ -0,0 +1,181 @@ + +# ********* +# |docname| +# ********* +# Copyright (C) 2011 Bradley N. Miller +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +__author__ = "jaltekruse" + +from docutils import nodes +from docutils.parsers.rst import directives +from sqlalchemy import Table +from runestone.server.componentdb import ( + addQuestionToDB, + addHTMLToDB, + maybeAddToAssignment, +) +from runestone.common.runestonedirective import ( + RunestoneIdDirective, + RunestoneIdNode, +) + + +def setup(app): + app.add_directive("doenet", DoenetDirective) + app.add_node(DoenetNode, html=(visit_hp_html, depart_hp_html)) + + +TEMPLATE_START = """ +
+
+
+""" + +TEMPLATE_END = """ +
+
+ +
+
+""" + + +class DoenetNode(nodes.General, nodes.Element, RunestoneIdNode): + pass + + +# self for these functions is an instance of the writer class. For example +# in html, self is sphinx.writers.html.SmartyPantsHTMLTranslator +# The node that is passed as a parameter is an instance of our node class. +def visit_hp_html(self, node): + + node["delimiter"] = "_start__{}_".format(node["runestone_options"]["divid"]) + + self.body.append(node["delimiter"]) + + res = TEMPLATE_START % node["runestone_options"] + self.body.append(res) + + +def depart_hp_html(self, node): + res = TEMPLATE_END % node["runestone_options"] + self.body.append(res) + + addHTMLToDB( + node["runestone_options"]["divid"], + node["runestone_options"]["basecourse"], + "".join(self.body[self.body.index(node["delimiter"]) + 1 :]), + ) + + self.body.remove(node["delimiter"]) + + +class DoenetDirective(RunestoneIdDirective): + """ + + 1+3000=4 + """ + + required_arguments = 1 + optional_arguments = 1 + has_content = True + option_spec = RunestoneIdDirective.option_spec.copy() + option_spec.update( + { + "dburl": directives.unchanged, + "language": directives.unchanged, + "reuse": directives.flag, + "randomize": directives.flag, + "blockanswer": directives.unchanged, + } + ) + + def run(self): + super(DoenetDirective, self).run() + addQuestionToDB(self) + + env = self.state.document.settings.env + + if "language" in self.options: + self.options["language"] = "data-language='{}'".format( + self.options["language"] + ) + else: + self.options["language"] = "" + + if "reuse" in self.options: + self.options["reuse"] = ' data-reuse="true"' + else: + self.options["reuse"] = "" + + if "randomize" in self.options: + self.options["randomize"] = ' data-randomize="true"' + else: + self.options["randomize"] = "" + + if "blockanswer" in self.options: + self.options["blockanswer"] = "data-blockanswer='{}'".format( + self.options["blockanswer"] + ) + else: + self.options["blockanswer"] = "" + + explain_text = None + if self.content: + if "~~~~" in self.content: + idx = self.content.index("~~~~") + explain_text = self.content[:idx] + self.content = self.content[idx + 1 :] + source = "\n".join(self.content) + else: + source = "\n" + + self.explain_text = explain_text or ["Not an Exercise"] + + self.options["initialsetting"] = source + + # SQL Options + if "dburl" in self.options: + self.options["dburl"] = "data-dburl='{}'".format(self.options["dburl"]) + else: + self.options["dburl"] = "" + + course_name = env.config.html_context["course_id"] + divid = self.options["divid"] + + hpnode = DoenetNode() + hpnode["runestone_options"] = self.options + hpnode["source"], hpnode["line"] = self.state_machine.get_source_and_line( + self.lineno + ) + self.add_name(hpnode) # make this divid available as a target for :ref: + + maybeAddToAssignment(self) + if explain_text: + self.updateContent() + self.state.nested_parse(explain_text, self.content_offset, hpnode) + + return [hpnode] diff --git a/bases/rsptx/interactives/runestone/doenet/js/doenet.js b/bases/rsptx/interactives/runestone/doenet/js/doenet.js new file mode 100644 index 00000000..5d981dba --- /dev/null +++ b/bases/rsptx/interactives/runestone/doenet/js/doenet.js @@ -0,0 +1,141 @@ +"use strict"; + +import RunestoneBase from "../../common/js/runestonebase.js"; +// TODO fix this, in the meantime including from sphinx_static_files.html +// ERROR in ./runestone/doenet/js/doenet-standalone.js 240673:19 +//Module parse failed: Unterminated template (240673:19) +//You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +//import "./doenet-standalone.js"; + +console.log("IN DOENET - add event listener for splice"); +// SPLICE Events +window.addEventListener("message", (event) => { + console.log("IN DOENET - got a message", event); + if (event.data.subject == "SPLICE.reportScoreAndState") { + console.log(event.data.score); + console.log(event.data.state); + event.data.course_name = eBookConfig.course; + event.data.div_id = event.data.state.activityId; + let ev = { + event: "doenet", + div_id: event.data.state.activityId, + percent: event.data.score, + correct: event.data.score == 1 ? 'T' : 'F', + act: JSON.stringify(event.data), + answer: JSON.stringify(event.data), + }; + window.componentMap[event.data.state.activityId].logBookEvent(ev); + } else if (event.data.subject == "SPLICE.sendEvent") { + console.log(event.data.location); + console.log(event.data.name); + console.log(event.data.data); + } +}); + +// separate into constructor and init +export class Doenet extends RunestoneBase { + constructor(opts) { + super(opts); + console.log(opts); + console.log("Jason update oct 24th"); + this.doenetML = opts.doenetML; + console.log("opts.orig.id", opts.orig.id); + var orig = $(opts.orig).find("div")[0]; + console.log(orig); + console.log(orig.id); + console.log(`${eBookConfig.new_server_prefix}/logger/bookevent`); + // todo - think about how we pass around the doenetML + //window.renderDoenetToContainer(orig, this.doenetML); + + var loadPageStateUrl = `/ns/assessment/getDoenetState?div_id=${opts.orig.id}&course_name=${eBookConfig.course}&event=doenet` + // data.div_id = this.divid; + // data.course = eBookConfig.course; + // data.event = eventInfo; + + window.renderDoenetToContainer(orig, this.doenetML, { + flags: { + // showCorrectness, + // readOnly, + // showFeedback, + // showHints, + showCorrectness: true, + readOnly: false, + solutionDisplayMode: "button", + showFeedback: true, + showHints: true, + allowLoadState: false, + allowSaveState: true, + allowLocalState: false, + allowSaveSubmissions: true, + allowSaveEvents: false, + autoSubmit: false, + }, + addBottomPadding: false, + activityId: opts.orig.id, + apiURLs: { + postMessages: true, + loadPageState: loadPageStateUrl + }, + }); + + //this.checkServer("hparsonsAnswer", true); + } + + async logCurrentAnswer(sid) {} + + renderFeedback() {} + + disableInteraction() {} + + checkLocalStorage() {} + setLocalStorage() {} + + restoreAnswers(data) { + console.log("TODO IMPLEMENT loading data from doenet activity", data); + } +} + +// +// Page Initialization +// + +$(document).on("runestone:login-complete", function () { + //ACFactory.createScratchActivecode(); + $("[data-component=doenet]").each(function () { + if ($(this).closest("[data-component=timedAssessment]").length == 0) { + // If this element exists within a timed component, don't render it here + try { + window.componentMap[this.id] = new Doenet({orig : this}); + // ACFactory.createActiveCode( + // this, + // $(this).find("textarea").data("lang") + // ); + } catch (err) { + console.log(`Error rendering Activecode Problem ${this.id} + Details: ${err}`); + } + } + }); + // The componentMap can have any component, not all of them have a disableSaveLoad + // method or an enableSaveLoad method. So we need to check for that before calling it. + // if (loggedout) { + // for (let k in window.componentMap) { + // if (window.componentMap[k].disableSaveLoad) { + // window.componentMap[k].disableSaveLoad(); + // } + // } + // } else { + // for (let k in window.componentMap) { + // if (window.componentMap[k].enableSaveLoad) { + // window.componentMap[k].enableSaveLoad(); + // } + // } + // } +}); + +if (typeof window.component_factory === "undefined") { + window.component_factory = {}; +} +window.component_factory.doenet = (opts) => { + return new Doenet(opts); +}; diff --git a/bases/rsptx/interactives/webpack.index.js b/bases/rsptx/interactives/webpack.index.js index b7eaeb18..42e2fac9 100644 --- a/bases/rsptx/interactives/webpack.index.js +++ b/bases/rsptx/interactives/webpack.index.js @@ -64,6 +64,7 @@ const module_map = { import("./runestone/clickableArea/js/timedclickable.js"), codelens: () => import("./runestone/codelens/js/codelens.js"), datafile: () => import("./runestone/datafile/js/datafile.js"), + doenet: () => import("./runestone/doenet/js/doenet.js"), dragndrop: () => import("./runestone/dragndrop/js/timeddnd.js"), fillintheblank: () => import("./runestone/fitb/js/timedfitb.js"), groupsub: () => import("./runestone/groupsub/js/groupsub.js"), @@ -87,6 +88,7 @@ const module_map = { // TODO: since this isn't in a ``data-component``, need to trigger an import of this code manually. webwork: () => import("./runestone/webwork/js/webwork.js"), youtube: () => import("./runestone/video/js/runestonevideo.js"), + doenet: () => import("./runestone/doenet/js/doenet.js"), }; const module_map_cache = {}; diff --git a/bases/rsptx/web2py_server/applications/runestone/controllers/admin.py b/bases/rsptx/web2py_server/applications/runestone/controllers/admin.py index 6716e8f2..699aee09 100644 --- a/bases/rsptx/web2py_server/applications/runestone/controllers/admin.py +++ b/bases/rsptx/web2py_server/applications/runestone/controllers/admin.py @@ -44,6 +44,7 @@ codelens=ALL_AUTOGRADE_OPTIONS, datafile=[], dragndrop=["manual", "all_or_nothing", "pct_correct", "interact"], + doenet=ALL_AUTOGRADE_OPTIONS, external=[], fillintheblank=ALL_AUTOGRADE_OPTIONS, khanex=ALL_AUTOGRADE_OPTIONS, @@ -85,6 +86,7 @@ clickablearea=ALL_WHICH_OPTIONS, codelens=ALL_WHICH_OPTIONS, datafile=[], + doenet=ALL_WHICH_OPTIONS, dragndrop=ALL_WHICH_OPTIONS, external=[], fillintheblank=ALL_WHICH_OPTIONS, diff --git a/bases/rsptx/web2py_server/applications/runestone/models/db_ebook.py b/bases/rsptx/web2py_server/applications/runestone/models/db_ebook.py index 528f9a2c..87337b56 100644 --- a/bases/rsptx/web2py_server/applications/runestone/models/db_ebook.py +++ b/bases/rsptx/web2py_server/applications/runestone/models/db_ebook.py @@ -262,6 +262,18 @@ migrate=bookserver_owned("microparsons_answers"), ) +db.define_table( + "doenet_answers", + Field("timestamp", "datetime"), + Field("div_id", "string"), + Field("sid", "string"), + Field("course_name", "string"), + Field("answer", "json"), + Field("correct", "boolean"), + Field("percent", "double"), + migrate=bookserver_owned("doenet_answers"), +) + # payments # -------- diff --git a/bases/rsptx/web2py_server/applications/runestone/modules/questions_report.py b/bases/rsptx/web2py_server/applications/runestone/modules/questions_report.py index a8eabaa9..e738e170 100644 --- a/bases/rsptx/web2py_server/applications/runestone/modules/questions_report.py +++ b/bases/rsptx/web2py_server/applications/runestone/modules/questions_report.py @@ -179,6 +179,12 @@ def questions_to_grades( & (db.useinfo.div_id == db.mchoice_answers.div_id) & (db.mchoice_answers.course_name == course_name) ), + db.doenet_answers.on( + (db.useinfo.timestamp == db.doenet_answers.timestamp) + & (db.useinfo.sid == db.doenet_answers.sid) + & (db.useinfo.div_id == db.doenet_answers.div_id) + & (db.doenet_answers.course_name == course_name) + ), db.parsons_answers.on( (db.useinfo.timestamp == db.parsons_answers.timestamp) & (db.useinfo.sid == db.parsons_answers.sid) @@ -399,6 +405,12 @@ def ts_get(table): row.parsons_answers.correct, ts_get(row.parsons_answers), ) + elif question_type == "doenet": + return ( + row.doenet_answers.answer, + row.doenet_answers.correct, + ts_get(row.doenet_answers), + ) elif question_type == "shortanswer": # Prefer data from the shortanswer table if we have it; otherwise, we can use useinfo's act. answer, ts = ( @@ -517,6 +529,9 @@ def query_assignment( db.parsons_answers.answer, db.parsons_answers.correct, db.parsons_answers.timestamp, + db.doenet_answers.answer, + db.doenet_answers.correct, + db.doenet_answers.timestamp, ##db.shortanswer_answers.answer, ##db.shortanswer_answers.timestamp, db.useinfo.timestamp, @@ -552,6 +567,10 @@ def query_assignment( (db.questions.question_type == "mchoice") & (db.question_grades.answer_id == db.mchoice_answers.id) ), + db.doenet_answers.on( + (db.questions.question_type == "doenet") + & (db.question_grades.answer_id == db.doenet_answers.id) + ), db.parsons_answers.on( (db.questions.question_type == "parsonsprob") & (db.question_grades.answer_id == db.parsons_answers.id) diff --git a/bases/rsptx/web2py_server/applications/runestone/modules/rs_grading.py b/bases/rsptx/web2py_server/applications/runestone/modules/rs_grading.py index 8b85ba63..3b1d55bb 100644 --- a/bases/rsptx/web2py_server/applications/runestone/modules/rs_grading.py +++ b/bases/rsptx/web2py_server/applications/runestone/modules/rs_grading.py @@ -152,6 +152,17 @@ def _score_one_microparsons(row, points, autograde): pct_correct = 0 return _score_from_pct_correct(pct_correct, points, autograde) +def _score_one_doenet(row, points, autograde): + # row is from doenet_answers + if autograde == "pct_correct" and "percent" in row and row.percent is not None: + pct_correct = int(round(row.percent * 100)) + else: + if row.correct: + pct_correct = 100 + else: + pct_correct = 0 + return _score_from_pct_correct(pct_correct, points, autograde) + def _score_one_fitb(row, points, autograde): # row is from fitb_answers @@ -401,6 +412,29 @@ def _scorable_microparsons_answers( query = query & (db.microparsons_answers.timestamp <= now) return db(query).select(orderby=db.microparsons_answers.timestamp) +def _scorable_doenet_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time=None, + db=None, + now=None, +): + query = ( + (db.doenet_answers.course_name == course_name) + & (db.doenet_answers.sid == sid) + & (db.doenet_answers.div_id == question_name) + ) + if deadline: + query = query & (db.doenet_answers.timestamp < deadline) + if practice_start_time: + query = query & (db.doenet_answers.timestamp >= practice_start_time) + if now: + query = query & (db.doenet_answers.timestamp <= now) + ret = db(query).select(orderby=db.doenet_answers.timestamp) + return ret def _scorable_fitb_answers( course_name, @@ -781,6 +815,21 @@ def _autograde_one_q( scoring_fn = _score_one_webwork logger.debug("AGDB - done with webwork") + elif question_type == "doenet": + logger.debug("grading a doenet!!") + results = _scorable_doenet_answers( + course_name, + sid, + question_name, + points, + deadline, + practice_start_time, + db=db, + now=now, + ) + scoring_fn = _score_one_doenet + logger.debug("AGDB - done with doenet") + elif question_type == "hparsons": logger.debug("grading a microparsons!!") results = _scorable_microparsons_answers( diff --git a/bases/rsptx/web2py_server/applications/runestone/static/js/admin.js b/bases/rsptx/web2py_server/applications/runestone/static/js/admin.js index 57c8e959..c99926ba 100644 --- a/bases/rsptx/web2py_server/applications/runestone/static/js/admin.js +++ b/bases/rsptx/web2py_server/applications/runestone/static/js/admin.js @@ -1743,6 +1743,16 @@ function display_write() { var hiddenwrite = document.getElementById("hiddenwrite"); hiddenwrite.style.visibility = "visible"; + + var hiddenDoenetEditor = document.getElementById("doenet-question-editor"); + var hiddenRunestoneEditor = document.getElementById("runestone-question-editor"); + if (questiontype === "doenet") { + hiddenDoenetEditor.style.display = "block"; + hiddenRunestoneEditor.style.display = "none"; + } else { + hiddenRunestoneEditor.style.display = "block"; + hiddenDoenetEditor.style.display = "none"; + } } function find_name(lines) { @@ -1759,7 +1769,7 @@ function find_name(lines) { } // Called when the "Done" button of the "Write" dialog is clicked. -function create_question(formdata) { +async function create_question(formdata) { if (formdata.qchapter.value == "Chapter") { alert("Please select a chapter for this question"); return; @@ -1767,6 +1777,29 @@ function create_question(formdata) { if (formdata.createpoints.value == "") { formdata.createpoints.value = "1"; } + var question; + if (formdata.template.value == "doenet") { + question = (await returnAllStateVariables1())['/_codeeditor1'].stateValues.text; + var questionId = "doenet-" + Math.floor(Math.random() * 10000000); + question = "" + question; + + formdata.qrawhtml.value = + `
+
+ +
+
` + } else { + question = formdata.qcode.value; + } + + var lines = question.split("\n"); + var name = find_name(lines); + + if (formdata.template.value == "doenet") { + } if (!formdata.qrawhtml.value) { alert("No HTML for this question, please generate it."); return; @@ -1777,11 +1810,7 @@ function create_question(formdata) { var assignmentid = select.options[select.selectedIndex].value; var assignmentname = select.options[select.selectedIndex].text; var template = formdata.template.value; - var qcode = formdata.qcode.value; - var lines = qcode.split("\n"); var htmlsrc = formdata.qrawhtml.value; - var name = find_name(lines); - var question = formdata.qcode.value; var difficulty = formdata.difficulty; for (var i = 0; i < difficulty.length; i++) { if (difficulty[i].checked == true) { @@ -1976,6 +2005,10 @@ async function renderRunestoneComponent(componentSrc, whereDiv, moreOpts) { ) { componentKind = "webwork"; } + + if (componentSrc.indexOf("doenet") >= 0) { + componentKind = "doenet"; + } // Import all the js needed for this component before rendering await runestoneComponents.runestone_import(componentKind); let opt = {}; @@ -1995,12 +2028,15 @@ async function renderRunestoneComponent(componentSrc, whereDiv, moreOpts) { } } - if (typeof component_factory === "undefined") { + if (typeof component_factory === "undefined" && componentKind != "doenet") { alert( "Error: Missing the component factory! probably a webpack version mismatch" ); } else { - if (!component_factory[componentKind] && !jQuery(`#${whereDiv}`).html()) { + if (false && componentKind == "doenet") { + console.log("wonder if I need to do something here"); + + } else if (!component_factory[componentKind] && !jQuery(`#${whereDiv}`).html()) { jQuery(`#${whereDiv}`).html( `

Preview not available for ${componentKind}

` ); @@ -2008,7 +2044,8 @@ async function renderRunestoneComponent(componentSrc, whereDiv, moreOpts) { try { let res = component_factory[componentKind](opt); res.multiGrader = moreOpts.multiGrader; - if (componentKind === "activecode") { + if (componentKind === "activecode" + || componentKind == "doenet") { if (moreOpts.multiGrader) { window.componentMap[`${moreOpts.gradingContainer} ${res.divid}`] = res; } else { @@ -2090,7 +2127,13 @@ async function renderRunestoneComponent(componentSrc, whereDiv, moreOpts) { } // $(`#${whereDiv}`).css("background-color", "white"); } - MathJax.typeset([document.querySelector(`#${whereDiv}`)]); + + + if (componentKind == "doenet") { + //window.renderDoenetToContainer(document.querySelector(".doenetml-applet")); + } else { + MathJax.typeset([document.querySelector(`#${whereDiv}`)]); + } } // Called by the "Search" button in the "Search question bank" panel. diff --git a/bases/rsptx/web2py_server/applications/runestone/views/_sphinx_static_files.html b/bases/rsptx/web2py_server/applications/runestone/views/_sphinx_static_files.html index 11fbe101..7337188e 100644 --- a/bases/rsptx/web2py_server/applications/runestone/views/_sphinx_static_files.html +++ b/bases/rsptx/web2py_server/applications/runestone/views/_sphinx_static_files.html @@ -10,6 +10,9 @@ + + + {{ if 'ptx_js_version' in globals() and 'webwork_js_version' in globals(): }} diff --git a/bases/rsptx/web2py_server/applications/runestone/views/admin/assignments.html b/bases/rsptx/web2py_server/applications/runestone/views/admin/assignments.html index e4b6fe37..5887fa31 100644 --- a/bases/rsptx/web2py_server/applications/runestone/views/admin/assignments.html +++ b/bases/rsptx/web2py_server/applications/runestone/views/admin/assignments.html @@ -306,6 +306,9 @@
Search Question Bank
+ + + +