Skip to content
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

Add new question type for DoenetML #458

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
35 changes: 33 additions & 2 deletions bases/rsptx/book_server_api/routers/assessment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +69,23 @@
tags=["assessment"],
)

@router.get("/getDoenetState")
async def getdoenetstate(request: Request, div_id: str,
course_name: str, event: str,
user=Depends(auth_manager)
):
request_data = AssessmentRequest(course=course_name, div_id=div_id, event=event)
assessment_results = await get_assessment_results_internal(request_data, request, user)

if assessment_results is not None:
doenet_state = assessment_results["answer"]["state"]
doenet_state["success"] = True
doenet_state["loadedState"] = True
return JSONResponse( status_code=200, content=jsonable_encoder(doenet_state) )
else:
return JSONResponse(
status_code=200, content=jsonable_encoder({"loadedState": False, "success": True})
)

# getAssessResults
# ----------------
Expand All @@ -75,6 +94,18 @@ async def get_assessment_results(
request_data: AssessmentRequest,
request: Request,
user=Depends(auth_manager),
):
assessment_results = await get_assessment_results_internal(request_data, request, user)
if assessment_results is not None:
return make_json_response(detail=assessment_results)
else:
return make_json_response(detail="no data")


async def get_assessment_results_internal(
request_data: AssessmentRequest,
request: Request,
user=Depends(auth_manager),
):
# if the user is not logged in an HTTP 401 will be returned.
# Otherwise if the user is an instructor then use the provided
Expand All @@ -95,7 +126,7 @@ async def get_assessment_results(
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 make_json_response(detail="no data")
return None
ret = row.dict()
rslogger.debug(f"row is {ret}")
if "timestamp" in ret:
Expand All @@ -118,7 +149,7 @@ async def get_assessment_results(
ret["comment"] = grades.comment
ret["score"] = grades.score
rslogger.debug(f"Returning {ret}")
return make_json_response(detail=ret)
return ret


# Define a simple model for the gethist request.
Expand Down
2 changes: 1 addition & 1 deletion bases/rsptx/book_server_api/routers/rslogging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions bases/rsptx/interactives/runestone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
# -----------------------
Expand Down Expand Up @@ -251,6 +253,7 @@ def build(options):
"datafile": DataFile,
"disqus": DisqusDirective,
"dragndrop": DragNDrop,
"doenet": DoenetDirective,
"groupsub": GroupSubmission,
"hparsons": HParsonsDirective,
"parsonsprob": ParsonsProblem,
Expand Down
1 change: 1 addition & 0 deletions bases/rsptx/interactives/runestone/doenet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .doenet import *
104 changes: 104 additions & 0 deletions bases/rsptx/interactives/runestone/doenet/doenet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

# *********
# |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 <http://www.gnu.org/licenses/>.
#
__author__ = "jaltekruse"

from docutils import nodes
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_doenet_html, depart_doenet_html))


TEMPLATE_START = """
<div class=\"runestone\" data-component=\"doenet\" id=\"${questionId}\">
<div class="doenetml-applet">
<script type="text/doenetml">
"""

TEMPLATE_END = """
</script>
</div>
</div>
"""


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_doenet_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_doenet_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):
"""
<!-- .. doenet:: doenet-1
-->
1+3000=<answer>4</answer>
"""

required_arguments = 1
optional_arguments = 1
has_content = True
option_spec = RunestoneIdDirective.option_spec.copy()

def run(self):
super(DoenetDirective, self).run()
addQuestionToDB(self)

doenet_node = DoenetNode()
doenet_node["runestone_options"] = self.options
self.add_name(doenet_node) # make this divid available as a target for :ref:

maybeAddToAssignment(self)

return [doenet_node]
141 changes: 141 additions & 0 deletions bases/rsptx/interactives/runestone/doenet/js/doenet.js
Original file line number Diff line number Diff line change
@@ -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);
};
2 changes: 2 additions & 0 deletions bases/rsptx/interactives/webpack.index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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 = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading