maths / moodle-qtype_stack

Stack question type for Moodle
GNU General Public License v3.0
138 stars 147 forks source link

Marking the JsxGraph from the question #1101

Closed raedshorrosh closed 5 months ago

raedshorrosh commented 5 months ago

Before STACK-JS, i used to add correction marks to my jsxgraph plot this way:

1) i defined a variable in the question text using

2. inside the JsxGraph block i defined the function checkAnswer that marks the answers on the graph with ✓ or ☓.

  1. I call the function checkAnswer from the PRT using something like that:

[[jsxgraph width="0px" height="0px"]]

checkAnswer(1,1);

[[/jsxgraph]]

this is no longer working and i get: Uncaught (in promise) ReferenceError: checkAnswer is not defined . image

All my questions stopped working ! how to interact with the graph from the PRT as i used to do.

Thank you for your help

LukeLongworth commented 5 months ago

STACK-JS limits the information that is allowed to pass between the JSXGraph iframe and the main body of the question for security reasons. The main way to pass information back and forth is via binding functions like stack_jxg.bind_point. Perhaps you could add a hidden extra input, something like [[input:checkAnswer]], that is bound to the variable? This may or may not work with the PRT, I've never tried to link a graph in a PRT to an input from the question text.

It's also worth noting that in-line scripts are set to eventually be prohibited from CASText, so you may wish to look for alternative solutions anyway and gradually transition your question bank. I had some discussion with STACK devs here: #961, #1078 and #1074. I hope that's helpful :)

raedshorrosh commented 5 months ago

Thank you for your suggestion. So the idea is to change the value of the hidden variable inside the prt which sends a signal to the checkAnswer to display the marks. I hope this will work for me. I will try this. But the problem is how to get access to the input so i can change its value from inside the prt. I tried using the [[iframe]] block as in the documentation but it did not work either and i got some errors too in the console log... This that we lost communication with jsxgraph due to all the security constrains is very frustrating to me and probably to many others. If this continues to be the case, we may need to roll back to earlier versions of stack, one before Stack-js.

On Tue, 30 Jan 2024 at 10:50 PM Luke Longworth @.***> wrote:

STACK-JS limits the information that is allowed to pass between the JSXGraph iframe and the main body of the question for security reasons. The main way to pass information back and forth is via binding functions https://docs.stack-assessment.org/en/Authoring/JSXGraph/ like stack_jxg.bind_point. Perhaps you could add a hidden extra input, something like [[input:checkAnswer]], that is bound to the variable? This may or may not work with the PRT, I've never tried to link a graph in a PRT to an input from the question text.

It's also worth noting that in-line scripts are set to eventually be prohibited from CASText, so you may wish to look for alternative solutions anyway and gradually transition your question bank. I had some discussion with STACK devs here: #961 https://github.com/maths/moodle-qtype_stack/pull/961, #1078 https://github.com/maths/moodle-qtype_stack/pull/1078 and #1074 https://github.com/maths/moodle-qtype_stack/issues/1074. I hope that's helpful :)

— Reply to this email directly, view it on GitHub https://github.com/maths/moodle-qtype_stack/issues/1101#issuecomment-1917870028, or unsubscribe https://github.com/notifications/unsubscribe-auth/AVPYJO6TLKFH6EO722XUWXDYRFMI3AVCNFSM6AAAAABCRMXKLOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMJXHA3TAMBSHA . You are receiving this because you authored the thread.Message ID: @.***>

aharjula commented 5 months ago

Ok. So let's expand that list of Lukes with this bit of opinions from me (the one that forces STACK-JS onto you).

There are basically two relatively simple ways for doing this, and what was suggested in the form of a proper STACK input is the best one as it is the only one that provides unique identifiers so that you do not need to worry about collisions when multiple copies of the same question happen to exist on the same page. On the other hand, if you intentionally want to share information between questions that is also possible.

The route I would take would be to have a String type input in the question let's call it "flags", and in it, I would place a JSON encoded dictionary that could be updated by random [[javascript]] around the question (in particular inside the PRTs). The [[jsxgraph]] could then bind to that with a custom binding so that no matter in what order things execute, changes would happen correctly and so that you could change the flags with other scripts.

So in the PRTs we would see blocks like these (possibly multiple getting separate flags), sorry about typos I wrote this without testing:

[[javascript input-ref-flags="inputid"]]
let input = document.getElementById(inputid);
let val = {};
if (input.value != '') {
 val = JSON.parse(input.value);
}
val['flagX'] = true; /* Or anything else that is JSON serialisable. */
/* Replace the input value. */
input.value = JSON.stringify(val);
/* And finally send it to the world. */
input.dispatchEvent(new Event('change'));
[[/javascript]]

In the graph, we would then have something like this:

[[jsxgraph ....  input-ref-flags="flagsid"]]
....

/* Store flags in a dict matching the JSON dict in the input.*/
let flags = {};
let flagserialiser = () => JSON.stringify(flags);
let flagdeserialiser = (val) => {
 flags = JSON.parse(val);
 /* Then the logic to check if flags are up. */
 if (flags.hasOwnProperty('flagX') && flags['flagX'] == true) {
    /* Setup or update the mark for the true case. */
 } else {
   /* Setup or update the mark for the false case. */
 }
 /* Other flags... here... */
};
/* Then we bind that to the input. The last parameter is empty as nothing on the graph side triggers this. */
stack_jxg.custom_bind(flagsid, flagserialiser, flagdeserialiser, []);
...

That is the way I would do it. The easier alternative is to have a non-STACK input and do that same logic with it. You could do that by instead of [[input:flags]] having <input type="hidden" id="_flags" value="{}"/>, note the underscore it is necessary, and that would be a so-called fake-input like mentioned in many of the other examples. The price of this alternative is having to deal with the possibility of non-unique identifiers, and that is an issue that one might want to avoid.

The other route is to not have inputs and instead use the HTML page itself, but it uses Promises and requires some other means for coordination than the handy change events of those inputs. The way one could approach this is to have a hidden div-element with a known id, and then use the STACK-JS functions stack_js.get_content and stack_js.switch_content to access the innerHTML of that element. The problem with this approach is that it relies on the content encoding being sane and requires sensible timed initiation logic and I don't recommend doing that unless you are sharing significant amounts of data between STACK-JS frames.

raedshorrosh commented 5 months ago

Ok. So let's expand that list of Lukes with this bit of opinions from me (the one that forces STACK-JS onto you).

There are basically two relatively simple ways for doing this, and what was suggested in the form of a proper STACK input is the best one as it is the only one that provides unique identifiers so that you do not need to worry about collisions when multiple copies of the same question happen to exist on the same page. On the other hand, if you intentionally want to share information between questions that is also possible.

The route I would take would be to have a String type input in the question let's call it "flags", and in it, I would place a JSON encoded dictionary that could be updated by random [[javascript]] around the question (in particular inside the PRTs). The [[jsxgraph]] could then bind to that with a custom binding so that no matter in what order things execute, changes would happen correctly and so that you could change the flags with other scripts.

So in the PRTs we would see blocks like these (possibly multiple getting separate flags), sorry about typos I wrote this without testing:

[[javascript input-ref-flags="inputid"]]
let input = document.getElementById(inputid);
let val = {};
if (input.value != '') {
 val = JSON.parse(input.value);
}
val['flagX'] = true; /* Or anything else that is JSON serialisable. */
/* Replace the input value. */
input.value = JSON.stringify(val);
/* And finally send it to the world. */
input.dispatchEvent(new Event('change'));
[[/javascript]]

In the graph, we would then have something like this:

[[jsxgraph ....  input-ref-flags="flagsid"]]
....

/* Store flags in a dict matching the JSON dict in the input.*/
let flags = {};
let flagserialiser = () => JSON.stringify(flags);
let flagdeserialiser = (val) => {
 flags = JSON.parse(val);
 /* Then the logic to check if flags are up. */
 if (flags.hasOwnProperty('flagX') && flags['flagX'] == true) {
    /* Setup or update the mark for the true case. */
 } else {
   /* Setup or update the mark for the false case. */
 }
 /* Other flags... here... */
};
/* Then we bind that to the input. The last parameter is empty as nothing on the graph side triggers this. */
stack_jxg.custom_bind(flagsid, flagserialiser, flagdeserialiser, []);
...

That is the way I would do it. The easier alternative is to have a non-STACK input and do that same logic with it. You could do that by instead of [[input:flags]] having <input type="hidden" id="_flags" value="{}"/>, note the underscore it is necessary, and that would be a so-called fake-input like mentioned in many of the other examples. The price of this alternative is having to deal with the possibility of non-unique identifiers, and that is an issue that one might want to avoid.

The other route is to not have inputs and instead use the HTML page itself, but it uses Promises and requires some other means for coordination than the handy change events of those inputs. The way one could approach this is to have a hidden div-element with a known id, and then use the STACK-JS functions stack_js.get_content and stack_js.switch_content to access the innerHTML of that element. The problem with this approach is that it relies on the content encoding being sane and requires sensible timed initiation logic and I don't recommend doing that unless you are sharing significant amounts of data between STACK-JS frames.

thank you for your detailed reply ! I used your suggestion and was able to change the value of the input in the PRT which is a major advancement. In the jsxgraph side i did also according to your suggestion but it did not work and i got these errors in the log:

image we are using STACK 4.4.6. It seems to me this is related to #992

aharjula commented 5 months ago

Assuming you are not using SEB when developing it is more likely that 4.4.6 just does not have the custom_bind bind capabilities I used, or possibly you have some other failure in the code-breaking the graph. Assuming, your graph displays something and only seems to break at the point of those serialiser/deserialiser bits do check what the console says in more detail and do make sure that you check the console of the sandbox iframe. Those CSP-errors are likely to be visible on the "top"-console of the whole page but around that console you should find a dropdown which allows changing the console to one of the others, the names of those should be obvious...

In any case, you could also do the [[jsxgraph]] side thing like this, without the full binding logic:

let flags = {};
let flagdeserialiser = (val) => {
 flags = JSON.parse(val);
 /* Then the logic to check if flags are up. */
 if (flags.hasOwnProperty('flagX') && flags['flagX'] == true) {
    /* Setup or update the mark for the true case. */
 } else {
   /* Setup or update the mark for the false case. */
 }
 /* Other flags... here... */
};
document.getElementById(flagsid).addEventListener('change', flagdeserialiser);
flagdeserialiser(document.getElementById(flagsid).value);
//stack_jxg.custom_bind(flagsid, flagserialiser, flagdeserialiser, []);

Again check for typos, I am not testing this before writing...

raedshorrosh commented 5 months ago

Assuming you are not using SEB when developing it is more likely that 4.4.6 just does not have the custom_bind bind capabilities I used, or possibly you have some other failure in the code-breaking the graph. Assuming, your graph displays something and only seems to break at the point of those serialiser/deserialiser bits do check what the console says in more detail and do make sure that you check the console of the sandbox iframe. Those CSP-errors are likely to be visible on the "top"-console of the whole page but around that console you should find a dropdown which allows changing the console to one of the others, the names of those should be obvious...

In any case, you could also do the [[jsxgraph]] side thing like this, without the full binding logic:

let flags = {};
let flagdeserialiser = (val) => {
 flags = JSON.parse(val);
 /* Then the logic to check if flags are up. */
 if (flags.hasOwnProperty('flagX') && flags['flagX'] == true) {
    /* Setup or update the mark for the true case. */
 } else {
   /* Setup or update the mark for the false case. */
 }
 /* Other flags... here... */
};
document.getElementById(flagsid).addEventListener('change', flagdeserialiser);
flagdeserialiser(document.getElementById(flagsid).value);
//stack_jxg.custom_bind(flagsid, flagserialiser, flagdeserialiser, []);

Again check for typos, I am not testing this before writing...

this worked for me ! Thank you.

raedshorrosh commented 5 months ago

Here is the method based on the above code by Matti Harjula (many thanks):

This is the question (chemistry): image the student is given the change in the concentration with time of one of the materials in the reaction (the black curve) and also is given the initial concentration of the other materials. The student has to drag the point in the graph to a correct concentration at time 4 according to the given reaction.
After submitting the answer and the correct prt is activated, a hidden input field named answers is updated and an event was dispatched this is how it is done in the correct prt:

[[javascript input-ref-answers="inputid"]] if ({#fixed#}!=1) { let input = document.getElementById(inputid); /* Replace the input value. */ input.value = JSON.stringify({indx:1,mrk:1}); /* And finally send it to the world. */ input.dispatchEvent(new Event('change')); } [[/javascript]]

index:1 is for curve A, mrk:1 is for the correct mark

in jsxgraph block the same input-ref-answers='flagsid' was passed. then the following code to do the marking and receiving the event:

document.getElementById(flagsid).addEventListener('change', flagdeserialiser);

function flagdeserialiser() { if (!(answered)) try { let val = document.getElementById(flagsid).value; flags = JSON.parse(val); / Then the logic to check if flags are up. / let indx = flags.indx; let mrk = flags.mrk;

switch (indx) {
  case 1:
    if (mrk == 1) {
      markA = correct;
    } else {
      markA = incorrect;
    }
    ans++;
    answered = (ans == 3);
    board.update();
    break;
  case 2:
    if (mrk == 1) {
      markB = correct;
    } else {
      markB = incorrect;
    }
    ans++;
    answered = (ans == 3);
    board.update();
    break;
  case 3:
    if (mrk == 1) {
      markC = correct;
    } else {
      markC = incorrect;

    }
    ans++;
    answered = (ans == 3);
    board.update();
    break;
  case 4:
    if (mrk == 1) {
      markD = correct;
    } else {
      markD = incorrect;

    }
    ans++;
    answered = (ans == 3);
    board.update();
    break;
}

} catch (err) {} }