Skip to content
Snippets Groups Projects
Unverified Commit 5f0bae24 authored by Chris Sangwin's avatar Chris Sangwin Committed by GitHub
Browse files

Merge pull request #1190 from maths/proof-builder

Parson's v2: Extension to grids and groupings
parents 5ecde6c8 f0c98c27
No related branches found
No related tags found
No related merge requests found
Showing
with 1906 additions and 487 deletions
...@@ -11,16 +11,30 @@ button { ...@@ -11,16 +11,30 @@ button {
} }
.container { .container {
overflow: auto; display: flex;
flex-wrap: wrap;
} }
#usedList:empty { .usedList > li[data-id] {
height:50px; background-color: floralwhite; border: 2px solid rgb(155, 199, 206);
}
#usedList > li {
background-color:rgb(176, 221, 228); border-left: thick solid rgb(155, 199, 206); background-color:rgb(176, 221, 228); border-left: thick solid rgb(155, 199, 206);
float: left; float: left;
flex-shrink: 1;
}
.usedList.col-rigid > li[data-id] {
margin-left: 0;
}
.row {
margin-right: 0;
margin-left: 0;
margin-bottom: 0;
}
.col-rigid {
width: 175px;
flex: 0 0 auto;
display: inline-block
} }
#availableList > li { #availableList > li {
...@@ -36,16 +50,72 @@ button { ...@@ -36,16 +50,72 @@ button {
box-shadow:0px 0px 0px 3px rgb(226, 150, 9) inset; box-shadow:0px 0px 0px 3px rgb(226, 150, 9) inset;
} }
#usedList > .sortable-chosen { .usedList > .sortable-chosen {
box-shadow:0px 0px 0px 3px rgb(155, 199, 206) inset; box-shadow:0px 0px 0px 3px rgb(155, 199, 206) inset;
} }
#usedList > .header { .usedList > .header {
background-color: inherit; text-align: center; border-bottom: thick solid; height: 50px;
background-color: inherit; text-align: center; border-bottom: thick solid; padding: 10px;
display: inline-block;
}
.usedList > .index {
background-color: inherit; text-align: left; border-right: thick solid; display: inline-block;
margin-right: 12px;
} }
#availableList > .header { #availableList > .header {
min-height: 50px;
height: auto;
width: max(100%, 150px);
background-color: inherit; text-align: center; border-bottom: thick solid rgb(196, 131, 10); background-color: inherit; text-align: center; border-bottom: thick solid rgb(196, 131, 10);
padding: 10px; display: inline-block;
}
#availableList > .index {
min-height: 50px;
height: auto;
background-color: inherit; text-align: left; border-right: thick solid rgb(196, 131, 10);
padding: 10px; margin: 12px;
}
.grid-item {
height: 50px;
background-color: #fff;
border: solid 1px rgb(0,0,0,0.2);
padding: 10px;
margin: 12px;
display: flex;
text-align: center;
}
.grid-item-rigid {
height: 50px;
width: 150px;
background-color: #fff;
border: solid 1px rgb(0,0,0,0.2);
padding: 10px;
margin: 12px;
display: flex;
text-align: center;
}
#availableList > .grid-item-rigid.index {
width: 150px;
}
.usedList:empty {
height: 50px;
background-color: floralwhite; border: 1px solid rgb(155, 199, 206);
padding: 10px;
margin: 12px;
text-align: center;
display: flex;
}
.usedList.col-rigid:empty {
width: 150px;
} }
.parsons-button { .parsons-button {
......
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css");@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");@import url("../styles.css");body{background-color:inherit;} @import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css");@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");@import url("../styles.css");body{background-color:inherit;}
button{margin:2px 3px;}.container{overflow:auto;}#usedList:empty{height:50px;background-color:floralwhite;border:2px solid rgb(155,199,206);}#usedList>li{background-color:rgb(176,221,228);border-left:thick solid rgb(155,199,206);float:left;}#availableList>li{background-color:rgb(243,189,88);float:left;}#availableList:empty{height:50px;background-color:lightpink;}#availableList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(226,150,9)inset;}#usedList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(155,199,206)inset;}#usedList>.header{background-color:inherit;text-align:center;border-bottom:thick solid;}#availableList>.header{background-color:inherit;text-align:center;border-bottom:thick solid rgb(196,131,10);}.parsons-button{display:inline-flex;align-items:center;padding:0.5rem 0.75rem;margin:2px 2px;font-size:25px;line-height:1.5;text-align:center;white-space:nowrap;vertical-align:middle;user-select:none;cursor:pointer;background-color:#6c757d;border:1px solid#6c757d;color:#ffffff;border-radius:0.35rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;}.parsons-button:hover{background-color:#5a6268;border-color:#545b62;color:#ffffff;}.parsons-bin{width:33%;cursor:default;}.bin-icon{margin-right:0.75rem;}.drop-zone{vertical-align:middle;display:inline-flex;flex:1;border:2px dashed#6c757d;border-radius:0.25rem;background-color:lightgray;align-self:stretch;}.drop-zone>li{font-size:0.75rem;background-color:lightcoral;max-height:100%;max-width:100%;text-align:left;flex:1;box-shadow:0px 0px 0px 2px rgb(167,89,89)inset;}.sortable-warning{float:right;font-size:0.8em;margin:-0.3em-0.3em;} button{margin:2px 3px;}.container{display:flex;flex-wrap:wrap;}.usedList>li[data-id]{background-color:rgb(176,221,228);border-left:thick solid rgb(155,199,206);float:left;flex-shrink:1;}.usedList.col-rigid>li[data-id]{margin-left:0;}.row{margin-right:0;margin-left:0;margin-bottom:0;}.col-rigid{width:175px;flex:0 0 auto;display:inline-block}#availableList>li{background-color:rgb(243,189,88);float:left;}#availableList:empty{height:50px;background-color:lightpink;}#availableList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(226,150,9)inset;}.usedList>.sortable-chosen{box-shadow:0px 0px 0px 3px rgb(155,199,206)inset;}.usedList>.header{height:50px;background-color:inherit;text-align:center;border-bottom:thick solid;padding:10px;display:inline-block;}.usedList>.index{background-color:inherit;text-align:left;border-right:thick solid;display:inline-block;margin-right:12px;}#availableList>.header{min-height:50px;height:auto;width:max(100%,150px);background-color:inherit;text-align:center;border-bottom:thick solid rgb(196,131,10);padding:10px;display:inline-block;}#availableList>.index{min-height:50px;height:auto;background-color:inherit;text-align:left;border-right:thick solid rgb(196,131,10);padding:10px;margin:12px;}.grid-item{height:50px;background-color:#fff;border:solid 1px rgb(0,0,0,0.2);padding:10px;margin:12px;display:flex;text-align:center;}.grid-item-rigid{height:50px;width:150px;background-color:#fff;border:solid 1px rgb(0,0,0,0.2);padding:10px;margin:12px;display:flex;text-align:center;}#availableList>.grid-item-rigid.index{width:150px;}.usedList:empty{height:50px;background-color:floralwhite;border:1px solid rgb(155,199,206);padding:10px;margin:12px;text-align:center;display:flex;}.usedList.col-rigid:empty{width:150px;}.parsons-button{display:inline-flex;align-items:center;padding:0.5rem 0.75rem;margin:2px 2px;font-size:25px;line-height:1.5;text-align:center;white-space:nowrap;vertical-align:middle;user-select:none;cursor:pointer;background-color:#6c757d;border:1px solid#6c757d;color:#ffffff;border-radius:0.35rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;}.parsons-button:hover{background-color:#5a6268;border-color:#545b62;color:#ffffff;}.parsons-bin{width:33%;cursor:default;}.bin-icon{margin-right:0.75rem;}.drop-zone{vertical-align:middle;display:inline-flex;flex:1;border:2px dashed#6c757d;border-radius:0.25rem;background-color:lightgray;align-self:stretch;}.drop-zone>li{font-size:0.75rem;background-color:lightcoral;max-height:100%;max-width:100%;text-align:left;flex:1;box-shadow:0px 0px 0px 2px rgb(167,89,89)inset;}.sortable-warning{float:right;font-size:0.8em;margin:-0.3em-0.3em;}
\ No newline at end of file \ No newline at end of file
File moved
This diff is collapsed.
This diff is collapsed.
# Authoring drag-and-drop matching and grid problems
The drag-and-drop functionality developed for [Parson's problems for proof](Parsons.md) has been extended to be used for general matching and grid problems. To author these, use the `[[parsons]] ... [[/parsons]]` block as one would for Parson's problems, but specify either `columns = "n"` or `columns="n"` _and_ `rows="m"`, which will set up a drag-and-drop grouping or grid layout with the number columns and rows as specified. You cannot specify `rows` without specifying `columns`.
The combinations of these two parameters define three possible layout configurations as follows:
1. **Proof** (`[[parsons]] [[/parsons]]`) : if both `columns` and `rows` are unspecified then this will give the traditional Parson's proof layout with an answer list that users must drag to and an available list that users must drag from. Refer to [the guide](Parsons.md) for writing Parson's proof questions.
2. **Column grouping** (`[[parsons columns="n"]] [[/parsons]]`) : If `columns="n"` is specified and `rows` is unspecified, this will lay out `n` _vertically arranged_ answer lists that items must be dragged to and an additional vertical available list that items must be dragged from. In this case, the lists can be arbitrary length and must be grown from the top downwards just as in the **Proof** layout. Via the reorientation button, the student is able to switch orientation between this and a row grouping setting (where answer lists are arranged as rows that are arbitrary length and grow from the left rightwards).
4. **Grid** (`[[parsons columns="n" rows="m"]] [[/parsons]]`) : If both `columns="n"` and `rows="m"` are specified, then this will lay out an `m` by `n` answer grid that items must be dragged to and a vertical available list that items must be dragged from. In this case, individual items can be passed to any position in the grid. The user also has the option to re-orient the grid to have `m` columns and `n` rows via the reorientation button.
The basic usage of all four modes are the exact same as [the Proof case](Parsons.md#authoring-json-within-the-question-text-itself), one can just modify the block parameters as specified. For example
```
[[parsons columns="2"]]
{
"f" : "\\(y = x^2\\)",
"g" : "\\(y = x^3\\)",
"quad" : "Quadratic",
"cubic" : "Cubic",
}
[[/parsons]]
```
## Clone mode
We emphasise that items can be re-used by setting `clone="true"` in the block header (as in `[[parsons columns="n" clone="true"]][[/parsons]]`). This is more likely to be needed for grouping and grid setups.
## Transposing on load
The re-orientation button allows the student to switch between vertical and horizontal orientation as they wish, but on load the default is for the columns to be displayed vertically. Using `transpose="true"` in the header (as in `[[parsons columns="n" transpose="true"]][[/parsons]]`) will change this so that the horizontal orientation will display on load.
## Headers
By default, answer lists in groupings and grid layouts will get default headers indexed by positive whole numbers. The available list will get a default header of "Drag from here:". These will become row indexes in **Row grouping** layout, or when the user presses the re-orient button.
Answer list headers can be changed by assigning the key `"headers"` key an an array of strings containing the new headers. The single header for the available list can be changed by assigning the `"available_header"` key to a string.
```
[[parsons columns="2"]]
{
"steps": {
"f" : "\\(y = x^2\\)",
"g" : "\\(y = x^3\\)",
"quad" : "Quadratic",
"cubic" : "Cubic",
},
"headers" : ["Equation", "Type"],
"available_header" : "Available items"
}
[[/parsons]]
```
Note that `headers.` must be a list of the same length as the number of columns and `available` must be a string.
Beware that long headers may overflow boxes when using several columns so it is best to keep them short.
## Index
By default in **Column grouping** and **grid** layouts no index is used. In **Row grouping** mode the headers are an index, and in this case no headers exist by default.
To change this, one can pass an index to the JSON as follows:
```
[[parsons columns="1" rows="2"]]
{
"steps": {
"quad" : "Quadratic",
"cubic" : "Cubic",
},
"headers" : ["Type"]
"available_header" : "Available items"
"index" : ["Equation", "\\(y = x^2\\)", "\\(y = x^3\\)"]
}
[[/parsons]]
```
Note that the length of the index must be the same as `rows + 1`. You can simply pass an empty string to the first position if no index header is required.
## Sortable options
The final JSON key allowed inside the `parsons` block is `"options"` whose value can be a JSON containing options that can be used to customise the functionality of the drag-and-drop list. See [the Parsons guide](Parsons.md) for how to include these, and [the Sortable library](https://github.com/SortableJS/Sortable#options) for further details on possible customisations.
## Full list of block parameters
See [the Parsons authoring guide](Parsons.md#block-parameters) for a full list of supported block parameters.
## Troubleshooting
If your matching problem is not displaying properly, in particular if the all the items are displayed in a single yellow block, then
double-check that you have spelled the keys of the JSON inside the Parsons block correctly as described below. They should be a subset of
```
{"steps", "options", "headers", "available_header", "index"}
```
and a superset of
```
{"steps"}
```
For technical reasons this is one error that we are unable to validate currently.
## State
The state of the problem at any given point in time during question answer takes on the following format:
```
{used: usedState, available: availableState}
```
where `usedState` and `availableState` are arrays containing the keys specified in `steps` of the JSON in the answer. In all cases, `availableState` is a flat array of variable length. The shape of `usedState`` depends on which of the four layouts is being used. We give examples below.
1. **Proof**: In this case `usedState` will have shape `(1, 1, ?)`, where `?` indicates the variable dimension. For example:
````
[[parsons input="ans1"]]
{
"1":"Assume that \\(n\\) is odd.",
"2":"Then there exists an \\(m\\in\\mathbb{Z}\\) such that \\(n=2m+1\\).",
"3":"\\[ n^2 = (2m+1)^2 = 2(2m^2+2m)+1.\\]",
"4":"Define \\(M=2m^2+2m\\in\\mathbb{Z}\\) then \\(n^2=2M+1\\).",
}
[[/parsons]]
````
might have, at a given time, a state that looks like:
```
{
used : [
[
["1", "3"]
]
]
available :
["2", "4"]
}
```
2. **Column grouping**: In this case `usedState` will have shape `(n, 1, ?)`, where `n` is the number of columns and `?` indicates the variable dimension. For example:
```
[[parsons columns="2"]]
{
"f" : "\\(y = x^2\\)",
"g" : "\\(y = x^3\\)",
"quad" : "Quadratic",
"cubic" : "Cubic",
}
[[/parsons]]
```
might have, at a given time, a state that looks like:
```
{
used : [
[
["f"]
],
[
["quad", "cubic"]
]
],
available : ["g"]
}
```
3. **Row grouping** : In this case `usedState` will have shape `(m, 1, ?)`, where `m` is the number of rows and `?` indicates the variable dimension. The state of **Row grouping** is just the same as **Column grouping** if `m` and `n` are the same.
4. **Grid** : In this case `usedState` will have shape `(n, m, 1)`, where `n` is the number of columns and `m` is the number of rows. For example:
```
[[parsons columns="2" rows="3"]]
{
"f" : "\\(y = x^2\\)",
"g" : "\\(y = x^3\\)",
"h" : "\\(y = x^4\\)",
"quad" : "Quadratic",
"cubic" : "Cubic",
"quart" : "Quartic"
}
[[/parsons]]
```
might have, at a given time, a state that looks like:
```
{
used : [
[
["f"],
["g"],
[],
],
[
["quad"],
[],
["quart"]
]
],
available : ["h", "cubic"]
}
```
\ No newline at end of file
...@@ -24,7 +24,7 @@ Here is a basic example of use: ...@@ -24,7 +24,7 @@ Here is a basic example of use:
Assume the question author writes a list `proof_steps` of pairs `["key", "string"]` in Maxima (as in the examples), in the question variables with both the correct and incorrect strings. Assume the question author writes a list `proof_steps` of pairs `["key", "string"]` in Maxima (as in the examples), in the question variables with both the correct and incorrect strings.
```` ````
[parsons input="ans1" ]] [[parsons input="ans1" ]]
{# stackjson_stringify(proof_steps) #} {# stackjson_stringify(proof_steps) #}
[[/parsons]] [[/parsons]]
```` ````
...@@ -55,10 +55,8 @@ The `[[parsons]]` block is a wrapper for the javascript library "Sortable.js", o ...@@ -55,10 +55,8 @@ The `[[parsons]]` block is a wrapper for the javascript library "Sortable.js", o
```` ````
[[parsons input="ans1"]] [[parsons input="ans1"]]
{ "steps": {# stackjson_stringify(proof_steps) #}, { "steps": {# stackjson_stringify(proof_steps) #},
"options": {"header" : ["Custom header for the answer list", "Custom header for the available steps"], "options": {"sortable option 1" : value, ..., "sortable option n" : value},
"sortable option 1" : value, "headers" : ["Custom header for the answer list"],
...
"sortable option n" : value}
} }
[[/parsons]] [[/parsons]]
```` ````
...@@ -72,15 +70,28 @@ A list of all Sortable.js options can be found [here](https://github.com/Sortabl ...@@ -72,15 +70,28 @@ A list of all Sortable.js options can be found [here](https://github.com/Sortabl
```` ````
Most other Sortable options can be modified, except for `ghostClass`, `group` and `onSort` as these are required to be set for basic functionality. Most other Sortable options can be modified, except for `ghostClass`, `group` and `onSort` as these are required to be set for basic functionality.
The only non-Sortable option that may currently be customised is the `header` option. The default for these are: Note that if you enter an unknown sortable option or if an attempt to pass `ghostClass`, `group`, or `onSort` is made, then these will simply be ignored. A warning will be displayed on the question page to signify this situation.
The default for "headers" and "available_header" are:
```` ````
{ {
"header": ["Construct your solution here:", "Drag from here:"] "headers": ["Construct your solution here:"],
"available_header": ["Drag from here:"]
} }
```` ````
To modify these pass an array of length two, with first entry corresponding to the header for the answer list and the second entry corresponding to the header for the list of available steps.
Note that if you enter an unknown sortable option or if an attempt to pass `ghostClass`, `group`, or `onSort` is made, then these will simply be ignored. A warning will be displayed on the question page to signify this situation. #### Troubleshooting
If your Parson's problem is not displaying properly, in particular if the all the items are displayed in a single yellow block, then
double-check that you have spelled the keys of the JSON inside the Parsons block correctly as described above. They should be a subset of
```
{"steps", "options", "headers", "available_header"}
```
and a superset of
```
{"steps"}
```
For technical reasons this is one error that we are unable to validate currently.
### Block parameters ### Block parameters
...@@ -91,10 +102,12 @@ Functionality and styling can be customised through the use of block parameters. ...@@ -91,10 +102,12 @@ Functionality and styling can be customised through the use of block parameters.
3. `width`: string containing a positive float + a valid CSS unit (e.g.`"480px"`, `"100%"`, ...). Default is `"100%"`. This fixes the width of the window containing the drag-and-drop lists. 3. `width`: string containing a positive float + a valid CSS unit (e.g.`"480px"`, `"100%"`, ...). Default is `"100%"`. This fixes the width of the window containing the drag-and-drop lists.
4. `aspect-ratio`: string, containing a float between 0 and 1. This can be used with `height`/`length` _or_ `width` (not both) and automatically determines the value of the un-used parameter in accordance with the value of `aspect-ratio`; unset by default. An error will occur if one sets values for `aspect-ratio`, `width`, `height` _or_ `aspect-ratio`, `width`, `length`. 4. `aspect-ratio`: string, containing a float between 0 and 1. This can be used with `height`/`length` _or_ `width` (not both) and automatically determines the value of the un-used parameter in accordance with the value of `aspect-ratio`; unset by default. An error will occur if one sets values for `aspect-ratio`, `width`, `height` _or_ `aspect-ratio`, `width`, `length`.
5. `clone`: string of the form `"true"` or `"false"`. It is `"false"` by default. When `"false"` there are two lists and each proof step is "single-use", here the author must write all necessary proof steps even if they repeat; when `"true"` all proof steps are re-usable with no restrictions on how many times they are used, steps can only be dragged from the available list into the answer list, and there is a bin to tidy up steps. 5. `clone`: string of the form `"true"` or `"false"`. It is `"false"` by default. When `"false"` there are two lists and each proof step is "single-use", here the author must write all necessary proof steps even if they repeat; when `"true"` all proof steps are re-usable with no restrictions on how many times they are used, steps can only be dragged from the available list into the answer list, and there is a bin to tidy up steps.
6. `orientation`: string of the form `"horizontal"` or `"vertical"`. This can be used to fix the initial orientation shown to the user, `"horizontal"` will show lists side-by-side and `"vertical"` will show lists on top of each other. Note that there is a button on the page in which the user may switch the orientation to their preference while answering the question, so the `"orientation"` block parameter only determines the initial layout. It is `"horizontal"` by default. 6. `override-css`: string containing the location of a local CSS file contained in `question/type/stack/corsscripts/` directory in the format `cors://file-name` or a href to an external CSS stylesheet. This will override all CSS styling of the drag-and-drop listing, so it should be used with care. However, it can be used to customise the styling of the lists by writing one's own custom CSS file and passing in the location of that file to this parameter. This parameter is unset by default.
7. `override-css`: string containing the location of a local CSS file contained in `question/type/stack/corsscripts/` directory in the format `cors://file-name` or a href to an external CSS stylesheet. This will override all CSS styling of the drag-and-drop listing, so it should be used with care. However, it can be used to customise the styling of the lists by writing one's own custom CSS file and passing in the location of that file to this parameter. This parameter is unset by default. 7. `override-js`: string containing a local JavaScript library or a href to a cdn of a JavaScript library. This will overwrite the Sortable library used with the library identified by the string. This should be used if one wishes to use an updated version of the Sortable library, or adding functionality with a custom library. Note that the custom library will need to either extend or import the base Sortable functionality. Unset by default.
8. `override-js`: string containing a local JavaScript library or a href to a cdn of a JavaScript library. This will overwrite the Sortable library used with the library identified by the string. This should be used if one wishes to use an updated version of the Sortable library, or adding functionality with a custom library. Note that the custom library will need to either extend or import the base Sortable functionality. Unset by default. 8. `version`: string of the form `"local"` or `"cdn"`. Whether to use STACK's local copy of the Sortable library or whether to pull version 1.15.0 of Sortable from cdn. This is `"local"` by default.
9. `version`: string of the form `"local"` or `"cdn"`. Whether to use STACK's local copy of the Sortable library or whether to pull version 1.15.0 of Sortable from cdn. This is `"local"` by default. 9. `columns` : string containing an integer `"n"`. How many vertical answer lists to display. By default, this is not used. If it is specified, then the styling will change to a grid-format with multiple vertical answer lists of unspecified length.
10. `rows` : string containing an integer `"m"`. How many horizontal answer lists to display. By default, this is not used. If it is specified and `columns` is _not_ specified, this will change to a grid-format with multiple horizontal answer lists of unspecified width. If both `columns` and `rows` are specified then this will provide a fixed length and width grid format, where items can be dragged to any position in the grid in any order. You cannot specify `rows` without specifying `columns`.
11. `transpose` : `"true"` or `"false"`; `"false"` by default. While the student is able to re-orient between vertical and horizontal as they wish, the default on load is for columns to be vertical. If you wish them to default to being horizontal, then pass `transpose="true"`.
## Random generation of `proof_step` order ## Random generation of `proof_step` order
......
...@@ -34,27 +34,24 @@ TO-DO: ...@@ -34,27 +34,24 @@ TO-DO:
## Parson's block development track ## Parson's block development track
Next (v4.6.0) Next (v4.7.0)
1. Grid arrangement, e.g. fill in a 2*2 grid (for matching problems). 1. Nested lists (flat list vs. nested/tree) and different proof types -- iff, induction, etc. how do we indicate the different scaffolding for this?
2. Use syntax hint to set up a non-empty starting point. 2. Use syntax hint to set up a non-empty starting point.
3. Nested lists (flat list vs. nested/tree) 3. Create templates from the start for different proof types
4. CSS styling fix for automated feedback
Later Later
1. Different proof types -- iff, induction, etc. how do we indicate the different scaffolding for this? 1. Restrict blocks to fixed number of steps
2. Create templates from the start for different proof types 2. Allow student to select proof style (e.g. iff, contradiction) and pre-structure answer list accordingly
3. Restrict blocks to fixed number of steps 3. Allow some strings in the correct answer to be optional. Allow authors to input a weight for each item and use weighted D-L distance, e.g., weight of 0 indicates that a step is not required, but will not be considered incorrect if included.
4. Allow student to select proof style (e.g. iff, contradiction) and pre-structure answer list accordingly 4. Making use of third item in other ways? Hover over a proof step to reveal more information (e.g., this could come from the third item in the list and give a hint/definition)
5. Allow some strings in the correct answer to be optional. Allow authors to input a weight for each item and use weighted D-L distance, e.g., weight of 0 indicates that a step is not required, but will not be considered incorrect if included. 5. Allow students to mark items (e.g. as used or unneeded) or tick used items
6. Making use of third item in other ways? Hover over a proof step to reveal more information (e.g., this could come from the third item in the list and give a hint/definition) 6. Confirmation for delete all?
7. Allow students to mark items (e.g. as used or unneeded) or tick used items 7. Alternative styling/signalling for clone mode?
8. Confirmation for delete all? 8. Better support (and documentation) for bespoke grading functions.
9. Alternative styling/signalling for clone mode? 9. Hashing keys
10. Better support (and documentation) for bespoke grading functions. 10. Check sortable for keyboard accessibility (SM: Not built-in to Sortable currently: https://github.com/SortableJS/Sortable/issues/1951; however, it looks like it is do-able with some work https://robbymacdonell.medium.com/refactoring-a-sortable-list-for-keyboard-accessibility-2176b34a07f4)
11. Hashing keys
12. Check sortable for keyboard accessibility (SM: Not built-in to Sortable currently: https://github.com/SortableJS/Sortable/issues/1951; however, it looks like it is do-able with some work https://robbymacdonell.medium.com/refactoring-a-sortable-list-for-keyboard-accessibility-2176b34a07f4)
## For "inputs 2"? ## For "inputs 2"?
......
# Authoring drag and drop matching problems in STACK
The drag-and-drop functionality of the [Parson's](Parsons.md) block in STACK can also be used to write matching and other grid-based problems, which can be answered by drag and drop.
This page provides a quick-start guide to authoring matching problems with the `parsons` block.
As of STACK v4.6.0, the `parsons` block has three main configurations (each of which support further customisation) which can be achieved by setting appropriate block header parameters `columns` and `rows` as appropriate:
1. **Proof** (Example usage: `[[parsons]] ... [[/parsons]]`) : This was introduced in STACK v4.5.0, and a full guide can be found [here](Parsons.md).
2. **Grouping** (Example usage: `[[parsons columns="3"]] ... [[/parsons]]`) :
This will set up a number of columns, each of which behave similarly to the single left-hand column of the **Proof** configuration, where the student may drag and drop items starting at the top of each column.
This is useful when we are only interesting in grouping items together, and specific row positions do not matter, or when each column may have variable length.
3. **Grid** (Example usage: `[[parsons columns="3" rows="2"]] ... [[/parsons]]`) :
This will set up a grid of shape `(columns, rows)`, where the student may drag and drop items to any position in the grid.
Note that many **Grid** style questions can also be written using the **Grouping** setup.
The main difference between them is that **Grid** allows the student to drag any item to any position in the grid, regardless
of whether the item above it has been filled; **Grouping** on the other hand only allows students to drag items to the
end of the list within a column.
## Troubleshooting
If your matching problem is not displaying properly, in particular if the all the items are displayed in a single yellow block, then
double-check that you have spelled the keys of the JSON inside the Parsons block correctly as described below. They should be a subset of
```
{"steps", "options", "headers", "available_header", "index"}
```
and a superset of
```
{"steps"}
```
For technical reasons this is one error that we are unable to validate currently.
## Switching orientation
Parsons blocks will display columns vertically by default.
The student has the option to flip the orientation so that the columns become horizontal rows, and back again, through a button on the question page.
If you wish the columns to be horizontal rows as the question display default, then simply add `transpose="true"` to the block header (e.g., `[[parsons columns="3" transpose="true"]] ... [[/parsons]]`, will load a question with 3 horizontal rows).
## Providing a model answer
_The format of the model answer is fixed and independent of the orientation discussed in the previous section_.
It should be defined in _Question variables_ as a two-dimensional array that is always column-grouped.
For example if a model answer looks like
```
a | b | c
---|---|---
d | | e
---|---|---
f | |
```
Then this should be defined as
```
ans: [["A", "D", "F"], ["B"], ["C", "E"]];
```
where, e.g., `"A"` is the key identifier for string `"a"`.
## Cloning items
The `clone` header parameter has been available in `parsons` since v4.5.0 of STACK.
This can be set to `"true"` to allow items to be used more than once (e.g., `[[parsons columns="3" clone="true"]]...[[/parsons]]`).
## Headers and index
Column headers default to `0`, `1`, ..., `columns` when using **Grouping** or **Grid** configurations of the Parson's block, so we recommend to always set custom headers for these cases.
See the examples below for how to do this.
There is also the option of setting the index, which is possible in both **Grouping** and **Grid** configurations, but is more likely to be useful for the latter.
This will appear as a specially styled left-most column and fixes labels for the rows.
The index does not count as a column, so you should _decrease columns by one in the header parameters_.
The item that appears top-left in both the index and header should be included in the _index only_.
See the examples below for more details.
## Item style
In **Grouping** and **Grid** configurations, the height and width of individual items can be changed via the `item-height` and `item-width` header parameters. Parameter values should be a string containing a number, and this will set the pixels of the height/width.
The default is `50px` (`item-height="50"`) for height, and the width is automatically deduced from the page layout and number of columns for the vertical orientation, or it is `100px` (`item-width="100"`) for the horizontal configuration.
This may be needed if you have images or other large overflowing items (including header titles).
## The matching library
The `matchlib.mac` Maxima library contains a small number of functions that are required for basic functionality of assessing matching problems in STACK.
Essentially they translate the author answers to and back from JSON format expected by the drag-and-drop engine.
Be sure to include this and make use of it as detailed in the examples below.
We include some basic helper functions that can allow the author to specify whether they care or not about the order within and between rows or columns as follows.
- `ans: [["A", "B"], ["C", "D"], ["E", "F"]]`
- `match_column_set` : I don't care about the order within a column ->
```
match_column_set(ans) = [{"A", "B"}, {"C", "D"}, {"E", "F"}]
```
- `match_row_set` : I don't care about the order within a row (unlikely to be required but included for completeness) ->
```
match_row_set(ans) = [{"A", "C", "E"}, {"B", "D", "F"}]
```
- `match_set_column` : I don't care about the order of the columns (unlikely to be required but included for completeness) ->
```
match_set_column(ans) = {["A", "B"], ["C", "D"], ["E", "F"]}
```
- `match_set_row` : I don't care about the order of the rows ->
```
match_set_rows(ans) = {["A", "C", "E"], ["B", "D", "F"]}
```
- `match_transpose` : I would like to turn my answer into a row-grouped array ->
```
match_transpose(ans) = [["A", "C", "E"], ["B", "D", "F"]]
```
## Example 1 : Grouping example
In our first example, the student is asked to place functions, given as equations, into columns with the categories "Differentiable", "Continuous, not differentiable", "Discontinuous".
### Question variables
As a minimum it is recommended to include:
- Load the matching library.
- Define all items in the available list as a two-dimensional array, where each item is an array of the form `["<ID>", "<actual item contents>"]`.
You will use the `"<ID>"` string to write solutions and assess student inputs; the second item is what is displayed to the student.
- Randomly permute the available items.
- The headers that will appear on top of the answer columns.
- The correct answer as a two-dimensional array.
This should be column grouped.
For our example, the _Question variables_ field looks as follows.
```
stack_include("contribl://matchlib.mac");
steps : [
["sq", "\\(f(x) = x^2\\)"],
["sin", "\\(f(x) = \\sin(x)\\)"],
["abs", "\\(f(x) = |x|\\)"],
["sqrt", "\\(f(x) = \\sqrt(x)\\)"],
["rec", "\\(f(x) = 1/x\\)"],
["sgn", "\\(f(x) = \\text{sgn}(x)\\)"]
];
steps: random_permutation(steps);
headers: [
"Differentiable",
"Continuous, not differentiable",
"Discontinuous"
];
ans: [
["sq", "sin"],
["abs", "sqrt"],
["rec", "sgn"]
];
```
### Question text
Here we should:
- Write the question text itself.
- Open the `parsons` block with `input` and `columns` header parameters.
- Transfer the variables from _Question variables_ into a JSON inside the `parsons` block as appropriate.
- Close the `parsons` block.
- Set `style="display:none"` in the input div to hide the messy state from the student.
```
<p>Recall that a function may be differentiable, continuous but
not differentiable, or discontinuous. Drag the functions
to their appropriate category. </p>
[[parsons input="ans1" columns="3"]]
{
"steps" : {#stackjson_stringify(steps)#},
"headers" : {#headers#}
}
[[/parsons]]
<p style="display:none">[[input:ans1]] [[validation:ans1]]</p>
```
### Question note
A question note is required due to the random permutation of `steps`. We use:
```
{@map(first, steps)@}
```
### Input: ans1
1. The _Input type_ field should be **String**.
2. The _Model answer_ field should construct a JSON object from the teacher's answer `ta` using `match_correct(ans, steps)`.
3. Set the option _Student must verify_ to "no".
4. Set the option _Show the validation_ to "no".
5. Add `hideanswer` to _Extra options_.
Steps 3, 4 and 5 are strongly recommended, otherwise the student will see unhelpful code representing the underlying state of their answer.
### Potential response tree: prt1
Define the feedback variable
```
sans: match_interpret(ans1);
```
This provides the student response as a two-dimensional array of the same format as `ans`.
At this point the author may choose to assess by comparing `sans` and `ans` as they see fit.
In this case, the order _within_ a column really doesn't matter, but the order of the columns does of course.
So we may convert the columns of `sans` and `ans` to sets in feedback variables using `match_column_set` from `matchlib.mac`.
```
sans: match_column_set(sans);
ans: match_column_set(ans);
```
We can then do a regular algebraic equivalence test between `sans` and `ans`. You should turn the node to `Quiet: Yes`, otherwise the student will see unhelpful code if they the answer wrong.
## Example 2 : Grid example
Here, the student is asked to drag functions and their derivatives to relevant columns and rows.
This particular example could work as a grouping example in the vein of Example 1 above, however the key difference
here is that the student can drag an item to any position in the grid, whereas in grouping items can only be added
to the end of a growing column list.
Much of this example is very similar to Example 1 above, with the following key differences:
- The `parsons` block should include a specified `rows` parameter.
- The `match_correct` function should use `true` as a third parameter inside _Model answer_.
- The `match_interpret` function should use `true` as a third parameter inside the PRT.
- We also define our PRT answer test differently, since we care only about the order within a row being preserved.
However this difference is not _required_ and is due only to the nature of the question (i.e., what we want to assess from this question is
different from the one in Example 1), rather than from any system requirements.
### Question variables
As a minimum it is recommended to include:
- Load the matching library.
- Define all items in the available list as a two-dimensional array, where each item is an array of the form `["<ID>", "<actual item contents>"]`.
You will use the `"<ID>"` string to write solutions and assess student inputs; the second item is what is displayed to the student.
- Randomly permute the available items.
- The headers that will appear on top of the answer columns.
- The correct answer as a two-dimensional array. This should always be column grouped.
For our example, the _Question variables_ field looks as follows.
```
stack_include("contribl://matchlib.mac");
steps : [
["f", "\\(y = x^2\\)"],
["g", "\\(y = x^3\\)"],
["dfdx", "\\(y' = 2x\\)"],
["dgdx", "\\(y' = 3x^2\\)"],
["df2d2x", "\\(y'' = 2\\)"],
["dg2d2x", "\\(y'' = 6x\\)"]
];
steps: random_permutation(steps);
headers: [
"Function",
"\\(d/dx\\)",
"\\(d^2/d^2x\\)"
];
ans: [
["f", "g"],
["dfdx", "dgdx"],
["df2d2x", "dg2d2x"]
];
```
### Question text
Here we should:
- Write the question text itself.
- Open the `parsons` block with `input`, `columns` and `rows` header parameters.
- Transfer the variables from _Question variables_ into a JSON inside the `parsons` block as appropriate.
- Close the `parsons` block.
- Set `style="display:none"` in the input div to hide the messy state from the student.
```
<p>Drag the items to match up the functions with their derivatives. </p>
[[parsons input="ans1" columns="3" rows="2"]]
{
"steps" : {#stackjson_stringify(steps)#},
"headers" : {#headers#}
}
[[/parsons]]
<p style="display:none">[[input:ans1]] [[validation:ans1]]</p>
```
### Question note
A question note is required due to the random permutation of `steps`. We use:
```
{@map(first, steps)@}
```
### Input: ans1
1. The _Input type_ field should be **String**.
2. The _Model answer_ field should construct a JSON object from the teacher's answer `ta` using `match_correct(ans, steps, true)`.
3. Set the option _Student must verify_ to "no".
4. Set the option _Show the validation_ to "no".
5. Add `hideanswer` to _Extra options_.
Steps 3, 4 and 5 are strongly recommended, otherwise the student will see unhelpful code representing the underlying state
of their answer.
### Potential response tree: prt1
Define the feedback variable
```
sans: match_interpret(ans1, true);
```
This provides the student response as a two-dimensional array of the same format as `ans`.
At this point the author may choose to assess by comparing `sans` and `ans` as they see fit. In this case, the _order of the rows themselves_ really doesn't matter, but the order of the rows does of course. So we may convert the list of rows of `sans` and `ans` to a set in feedback variables using `match_set_row` from `matchlib.mac`.
```
sans: match_set_row(sans);
ans: match_set_row(ans);
```
We can then do a regular algebraic equivalence test between `sans` and `ans`.
You should turn the node to `Quiet: Yes`, otherwise the student will see unhelpful code if they the answer wrong.
## Example 3 : Grid example with an index
One can add a left-hand index to the grid in Example 2 by defining an `index` array in _Question variables_ and passing this through in the JSON inside the `parsons` block.
This will fix the row order and simplify assessment.
Important points:
- An item that appears in both the header and the index is **required**.
This item should appear in the index and not in the header.
- Reduce the value of the `columns` parameter in the `parsons` block by one: this corresponds only to the number of answer columns.
- Pass the index as the value of key `"index"` inside the JSON within the `parsons` block.
### Question variables
The question variables for Example 2 with an index is as follows.
```
stack_include("contribl://matchlib.mac");
steps : [
["dfdx", "\\(y' = 2x\\)"],
["dgdx", "\\(y' = 3x^2\\)"],
["df2d2x", "\\(y'' = 2\\)"],
["dg2d2x", "\\(y'' = 6x\\)"]
];
steps: random_permutation(steps);
headers: [
"\\(d/dx\\)",
"\\(d^2/d^2x\\)"
];
index: [
"Function",
"\\(y = x^2\\)",
"\\(y = x^3\\)"
]
ans: [
["dfdx", "dgdx"],
["df2d2x", "dg2d2x"]
];
```
### Question text
```
<p>Drag the items to match up the functions with their derivatives. </p>
[[parsons input="ans1" columns="2" rows="2"]]
{
"steps" : {#stackjson_stringify(steps)#},
"headers" : {#headers#},
"index" : {#index#}
}
[[/parsons]]
<p style="display:none">[[input:ans1]] [[validation:ans1]]</p>
```
### Question note
A question note is required due to the random permutation of `steps`. We use:
```
{@map(first, steps)@}
```
### Input
This is exactly the same as Example 2.
1. The _Input type_ field should be **String**.
2. The _Model answer_ field should construct a JSON object from the teacher's answer `ta` using `match_correct(ans, steps, true)`.
3. Set the option _Student must verify_ to "no".
4. Set the option _Show the validation_ to "no".
5. Add `hideanswer` to _Extra options_.
### PRT
As in Example 2, we first extract the two-dimensional array of used items from the students input.
```
sans: match_interpret(ans1, true);
```
At this point the author may choose to assess by comparing `sans` and `ans` as they see fit.
Since we have fixed the order of both dimensions, there is only one correct answer which is given by `ans`.
Hence we have a basic PRT which tests only algebraic equivalence between `sans` and `ans`.
As always, turn the node to `Quiet: Yes`, otherwise the student will see unhelpful code if they the answer wrong.
## Example 4 : Using images
Through the use of STACK's `plot` function, which wraps Maxima's `plot2d`, static images can also be included within items.
Apart from modifying the content of the steps of Example 2, the key difference here is that the width and height of the items must also be specified in the block parameter, to make sure that plots fit inside.
This can be done by specifying the `item-width` and `item-height` parameters within the block header of `parsons`.
Because of this, it is recommended to always specify the `[size, x, y]` option within `plot`, and add some padding to `x` and `y` to define the values of `item-width` and `item-height`.
Example 2 with plots rather than equations is given below.
### Question variables
```
stack_include("contribl://matchlib.mac");
steps: [
["f", plot(x^2,[x,-1,1], [size, 200, 200])],
["g", plot(x^3,[x,-1,1], [size, 200, 200])],
["dfdx", plot(2*x,[x,-1,1], [size, 200, 200])],
["dgdx", plot(3*x^2,[x,-1,1], [size, 200, 200])],
["df2d2x", plot(2,[x,-1,1], [size, 200, 200])],
["dg2d2x", plot(6*x,[x,-1,1], [size, 200, 200])]
];
steps: random_permutation(steps);
headers: ["Function", "\\(d/dx\\)", "\\(d^2/d^2x\\)"];
ans: [
["f", "g"],
["dfdx", "dgdx"],
["df2d2x", "dg2d2x"]
];
```
### Question text
```
<p>Drag the items to match up the functions with their derivatives. </p>
[[parsons input="ans1" columns="3" rows="2" item-height="250" item-width="250"]]
{
"steps" : {#stackjson_stringify(steps)#},
"headers" : {#headers#},
}
[[/parsons]]
<p style="display:none">[[input:ans1]] [[validation:ans1]]</p>
```
### Question note, Inputs and PRT
These are exactly the same as Example 2.
...@@ -33,6 +33,19 @@ Notes ...@@ -33,6 +33,19 @@ Notes
* Lists are a special case of a tree with one root (the list creation function) and an arbitrary number of nodes in order. Hence our design explicitly includes traditional Parson's problems as a special case. * Lists are a special case of a tree with one root (the list creation function) and an arbitrary number of nodes in order. Hence our design explicitly includes traditional Parson's problems as a special case.
* Teachers who do not want to scaffold explicit block structures (e.g. signal types of proof blocks) can choose to restrict students to (i) flat lists, or (ii) lists of lists. * Teachers who do not want to scaffold explicit block structures (e.g. signal types of proof blocks) can choose to restrict students to (i) flat lists, or (ii) lists of lists.
## Troubleshooting
If your Parson's problem is not displaying properly, in particular if the all the items are displayed in a single yellow block, then
double-check that you have spelled the keys of the JSON inside the Parsons block correctly as described below. They should be a subset of
```
{"steps", "options", "headers", "available_header"}
```
and a superset of
```
{"steps"}
```
For technical reasons this is one error that we are unable to validate currently.
# Example 1: a minimal Parson's question # Example 1: a minimal Parson's question
The following is a minimal Parson's question where there student is expected to create a list in one and only one order. The following is a minimal Parson's question where there student is expected to create a list in one and only one order.
......
...@@ -949,9 +949,22 @@ $string['stackBlock_parsons_unknown_named_version'] = 'The Parson\'s block only ...@@ -949,9 +949,22 @@ $string['stackBlock_parsons_unknown_named_version'] = 'The Parson\'s block only
$string['stackBlock_parsons_unknown_mathjax_version'] = 'The Parson\'s block only supports MathJax versions {$a->mjversion} for the mathjax parameter.'; $string['stackBlock_parsons_unknown_mathjax_version'] = 'The Parson\'s block only supports MathJax versions {$a->mjversion} for the mathjax parameter.';
$string['stackBlock_parsons_ref'] = 'The Parson\'s block only supports referencing inputs present in the same CASText section \'{$a->var}\' does not exist here.'; $string['stackBlock_parsons_ref'] = 'The Parson\'s block only supports referencing inputs present in the same CASText section \'{$a->var}\' does not exist here.';
$string['stackBlock_parsons_param'] = 'The Parson\'s block supports only these parameters in this context: \'{$a->param}\'.'; $string['stackBlock_parsons_param'] = 'The Parson\'s block supports only these parameters in this context: \'{$a->param}\'.';
$string['stackBlock_parsons_contents'] = 'The contents of a Parson\'s block must be a JSON of the form {#stackjson_stringify(proof_steps)#}. If you are passing custom objects then the Parson\'s block contents should be a JSON of the form {steps: {#stackjson_stringify(proof_steps)#}, options: {JSON containing Sortable options}}. Alternatively, the contents of the Parsons block may contain raw JSON equivalents. Make sure that the proof_steps Maxima variable is of the correct format. Note that all proof steps must be strings. See the documentation for details.'; $string['stackBlock_parsons_contents'] = 'The contents of a Parson\'s block must be a either a JSON of the form {#stackjson_stringify(steps)#}, where `steps` is the two-dimensional Maxima array containing key, value pairs of items, or of the form {\'steps\' : {#stackjson_stringify(steps)#}, \'options\' : {JSON containing Sortable options}, \'header\' : [List of headers], \'available_header\' : \'string containing header for the available list\', \'index\' : [List containing the index]}, where the \'options\', \'header\', \'available_header\', and \'index\' keys are optional. Alternatively, the contents of the Parsons block may contain raw JSON equivalents. Make sure that the `steps` Maxima variable is of the correct format. Note that all steps must be strings. See https://docs.stack-assessment.org/en/Authoring/Parsons/ for details.';
$string['stackBlock_incorrect_header_length'] = 'The list of headers should have the same length as the number of columns passed to the block header.';
$string['stackBlock_incorrect_available_header_type'] = 'The header for the available list should be passed as a string or a list of length one.';
$string['stackBlock_incorrect_index_length'] = 'The length of the index should be one more than the number of rows passed to the block header. An item in the top-left corner should always go in the index';
$string['stackBlock_incorrect_index_type'] = 'Index should be an array containing strings.';
$string['stackBlock_incorrect_header_type'] = 'Headers should be an array containing strings.';
$string['stackBlock_parsons_invalid_columns_value'] = 'The value of `columns` in the Parson\'s block header should be a string containing a positive integer.';
$string['stackBlock_parsons_invalid_rows_value'] = 'The value of `rows` in the Parson\'s block header should be a string containing a positive integer.';
$string['stackBlock_parsons_invalid_item-height_value'] = 'The value of `item-height` in the Parson\'s block header should be a string containing a positive integer.';
$string['stackBlock_parsons_invalid_item-width_value'] = 'The value of `item-width` in the Parson\'s block header should be a string containing a positive integer.';
$string['stackBlock_unknown_sortable_option'] = 'Unknown Sortable options found, the following are being ignored: '; $string['stackBlock_unknown_sortable_option'] = 'Unknown Sortable options found, the following are being ignored: ';
$string['stackBlock_overwritten_sortable_option'] = 'Unchangeable Sortable options found, the following are being ignored: '; $string['stackBlock_overwritten_sortable_option'] = 'Unchangeable Sortable options found, the following are being ignored: ';
$string['stackBlock_parsons_unknown_transpose_value'] = 'Transpose must be one of \'true\' or \'false\'.';
$string['stackBlock_parsons_underdefined_grid'] = 'When defining `rows` for a Parson\'s block one must also define `columns`.';
$string['stackBlock_proof_mode_index'] = 'The use of \'index\' is not supported when using the Parson\'s block for proof assessment.';
$string['stackBlock_proof_incorrect_header_length'] = 'Headers should be an array containing a single header; use \'available_header\' to update the header for the available list.';
// Define the stackBlock GeoGebra strings. // Define the stackBlock GeoGebra strings.
$string['stackBlock_geogebra_width'] = 'The width of a GeoGebra Applet must use a known CSS-length unit.'; $string['stackBlock_geogebra_width'] = 'The width of a GeoGebra Applet must use a known CSS-length unit.';
......
...@@ -34,7 +34,7 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -34,7 +34,7 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
], ],
'local' => [ 'local' => [
'css' => 'cors://sortable.min.css', 'css' => 'cors://sortable.min.css',
'js' => 'cors://sortable.min.js', 'js' => 'cors://sortablecore.min.js',
], ],
]; ];
...@@ -43,12 +43,44 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -43,12 +43,44 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
// Define iframe params. // Define iframe params.
$xpars = []; $xpars = [];
$inputs = []; // From inputname to variable name.
$clone = "false"; // Whether to have all keys in available list cloned. // Input identifiers.
$mathjaxversion = "2"; // MathJax version (either "2" or "3"). $inputs = [];
// Whether to have all keys in available list cloned.
$clone = 'false';
// MathJax version (either "2" or "3").
$mathjaxversion = '2';
// Number of available columns.
$columns = null;
// Number of available rows.
$rows = null;
// Tranpose.
$transpose = false;
// Item height.
$itemheight = null;
// Item width.
$itemwidth = null;
foreach ($this->params as $key => $value) { foreach ($this->params as $key => $value) {
if ($key === 'clone') { if ($key === 'clone') {
$clone = $value; $clone = $value;
} else if ($key === 'columns') {
$columns = $value;
} else if ($key === 'rows') {
$rows = $value;
} else if ($key === 'transpose') {
$transpose = ($value === 'true');
} else if ($key === 'item-height') {
$itemheight = $value;
} else if ($key === 'item-width') {
$itemwidth = $value;
} else if ($key !== 'input') { } else if ($key !== 'input') {
$xpars[$key] = $value; $xpars[$key] = $value;
} else { } else {
...@@ -65,9 +97,6 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -65,9 +97,6 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
if (isset($xpars['overridejs'])) { if (isset($xpars['overridejs'])) {
unset($xpars['overridejs']); unset($xpars['overridejs']);
} }
if (isset($xpars['orientation'])) {
unset($xpars['orientation']);
}
// Set default width and height here. // Set default width and height here.
// We want to push forward to overwrite the iframe defaults if they are not provided in the block parameters. // We want to push forward to overwrite the iframe defaults if they are not provided in the block parameters.
...@@ -126,15 +155,19 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -126,15 +155,19 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
} }
} }
// Identify default proof mode based on block header params
// Note that proof mode behaves the same as the general mode, but we just
// need to redefine columns.
$proofmode = ($columns === null && $rows === null);
$gridmode = !$proofmode;
$columns = $proofmode ? '1' : $columns;
// Add correctly oriented container divs for the proof lists to be accessed by sortable. // Add correctly oriented container divs for the proof lists to be accessed by sortable.
$orientation = isset($this->params['orientation']) ? $this->params['orientation'] : 'horizontal'; $orientation = $transpose ? 'row' : 'col';
$outer = $orientation === 'horizontal' ? 'row' : 'col'; $ogcolumns = $columns;
$inner = $orientation === 'horizontal' ? 'col' : 'row'; $ogrows = $rows;
$innerui = '<ul class="list-group ' . $inner . '" id="usedList"></ul> $columns = $transpose ? $ogrows : $ogcolumns;
<ul class="list-group ' . $inner . '" id="availableList"></ul>'; $rows = $transpose ? $ogcolumns : $ogrows;
$r->items[] = new MP_String("<button type='button' class='parsons-button' id='orientation'>
<i class='fa fa-refresh'></i></button>");
$r->items[] = new MP_String("<button type='button' class='parsons-button' id='resize'> $r->items[] = new MP_String("<button type='button' class='parsons-button' id='resize'>
<i class='fa fa-expand'></i></button>"); <i class='fa fa-expand'></i></button>");
if ($clone === 'true') { if ($clone === 'true') {
...@@ -144,28 +177,21 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -144,28 +177,21 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
<i class="fa fa-times-circle "></i></div>'); <i class="fa fa-times-circle "></i></div>');
} }
$r->items[] = new MP_String('<div class="container" id="sortableContainer" style="' . $astyle . '"> $r->items[] = new MP_String('<div class="container row" id="containerRow" style="' . $astyle . '"></div>');
<div class=row>' . $innerui . '
</div>
</div>');
// JS script. // JS script.
$r->items[] = new MP_String('<script type="module">'); $r->items[] = new MP_String('<script type="module">');
$importcode = "\nimport {stack_js} from '" . stack_cors_link('stackjsiframe.min.js') . "';\n"; $importcode = "\nimport {stack_js} from '" . stack_cors_link('stackjsiframe.min.js') . "';\n";
$importcode .= "import {Sortable} from '" . stack_cors_link('sortable.min.js') . "';\n"; $importcode .= "import {Sortable} from '" . stack_cors_link('sortablecore.min.js') . "';\n";
$importcode .= "import {preprocess_steps, $importcode .= "import {preprocess_steps,
stack_sortable, stack_sortable,
add_orientation_listener,
get_iframe_height, get_iframe_height,
SUPPORTED_CALLBACK_FUNCTIONS SUPPORTED_CALLBACK_FUNCTIONS
} from '" . } from '" .
stack_cors_link('stacksortable.min.js') . "';\n"; stack_cors_link('stacksortable.min.js') . "';\n";
$r->items[] = new MP_String($importcode); $r->items[] = new MP_String($importcode);
// Add flip orientation listener to the orientation button.
// TO-DO: automatically set orientation based on device?
$r->items[] = new MP_String('add_orientation_listener("orientation", "usedList", "availableList");' . "\n");
// Add the resize button listener. // Add the resize button listener.
$r->items[] = new MP_String('document.getElementById("resize").addEventListener( $r->items[] = new MP_String('document.getElementById("resize").addEventListener(
"click", () => {stack_js.resize_containing_frame("' . $width . '", get_iframe_height() + "px");});' . "\n"); "click", () => {stack_js.resize_containing_frame("' . $width . '", get_iframe_height() + "px");});' . "\n");
...@@ -189,17 +215,64 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -189,17 +215,64 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
} }
$r->items[] = new MP_String(";\n"); $r->items[] = new MP_String(";\n");
// Parse steps and Sortable options separately if they exist. Invalid JSON will be identified by preprocess_steps function. // Define default headers.
$code = 'var headers = {used: {header: "' . stack_string('stackBlock_parsons_used_header') . '"}, if ($proofmode) {
available: {header: "' . stack_string('stackBlock_parsons_available_header') . '"}};' . "\n"; $code = 'var headers = ["' . stack_string('stackBlock_parsons_used_header') . '"];' . "\n";
} else {
$code = 'var headers = [' . implode(', ', range(1, intval($ogcolumns))) . '];' . "\n";
}
$code .= 'var available_header = "' . stack_string('stackBlock_parsons_available_header') . '";' . "\n";
// Parse steps, Sortable options, headers and index separately if they exist.
// Invalid JSON will be identified by preprocess_steps function.
$code .= 'var sortableUserOpts = {};' . "\n"; $code .= 'var sortableUserOpts = {};' . "\n";
$code .= 'var valid;' . "\n"; $code .= 'var valid, index;' . "\n";
$code .= '[proofSteps, headers, sortableUserOpts, valid] = preprocess_steps(proofSteps, headers, sortableUserOpts);' . "\n";
$code .= '[proofSteps, sortableUserOpts, headers, available_header, index, valid] =
preprocess_steps(proofSteps, sortableUserOpts, headers, available_header, index);' . "\n";
// If the author's JSON has invalid format throw an error. // If the author's JSON has invalid structure throw an error.
$code .= 'if (valid === false) $code .= 'if (valid === false)
{stack_js.display_error("' . stack_string('stackBlock_parsons_contents') . '");}' . "\n"; {stack_js.display_error("' . stack_string('stackBlock_parsons_contents') . '");}' . "\n";
// More specific pieces of validation
// Check typing of headers, it should be an array containing strings.
$code .= 'if (!(Array.isArray(headers)))
{stack_js.display_error("' . stack_string('stackBlock_incorrect_header_type') . '");}' . "\n";
// If the length of headers does not match the number of columns expected throw an error.
// Error is different for proof vs. matching
$code .= 'if (headers.length !== ' . $ogcolumns . ') {stack_js.display_error("';
if ($proofmode) {
$code .= stack_string('stackBlock_proof_incorrect_header_length') . '");}' . "\n";
} else {
$code .= stack_string('stackBlock_incorrect_header_length') . '");}' . "\n";
}
// Validate available headers. It
// is either a string or an array containing a single string.
$code .= 'if (!(typeof(available_header) === "string" ||
(Array.isArray(available_header) && available_header.length === 1 && typeof(available_header[0]) === "string")))
{stack_js.display_error("' . stack_string('stackBlock_incorrect_available_header_type') . '");}' . "\n";
// Extract available header if it is an array containing a single string
$code .= 'if (Array.isArray(available_header)) {available_header = available_header[0]};' . "\n";
// If index is passed then it should be an array containing strings.
$code .= 'if (index !== undefined && !(Array.isArray(index) && index.every((idx) => typeof(idx) === "string")))
{stack_js.display_error("' . stack_string('stackBlock_incorrect_index_type') . '");}' . "\n";
// If rows and index are passed then the length of index should match the value of rows + 1
if ($ogrows !== null) {
$code .= 'if (index !== undefined && index.length !== ' . $ogrows + 1 . ')
{stack_js.display_error("' . stack_string('stackBlock_incorrect_index_length') . '");}' . "\n";
}
// Index cannot be used in proof mode due to styling issues
if ($proofmode) {
$code .= 'if (index !== undefined)
{stack_js.display_error("' . stack_string('stackBlock_proof_mode_index') . '");}' . "\n";
}
// Link up to STACK inputs. // Link up to STACK inputs.
if (count($inputs) > 0) { if (count($inputs) > 0) {
$code .= 'var inputPromise = stack_js.request_access_to_input("' . $this->params['input'] . '", true);'; $code .= 'var inputPromise = stack_js.request_access_to_input("' . $this->params['input'] . '", true);';
...@@ -210,22 +283,37 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -210,22 +283,37 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
}; };
// Instantiate STACK sortable helper class. // Instantiate STACK sortable helper class.
$code .= 'const stackSortable = new stack_sortable(proofSteps, "availableList", "usedList", id, sortableUserOpts, "' . $code .= 'const stackSortable = new stack_sortable(proofSteps, id, sortableUserOpts, "' .
$clone .'");' . "\n"; $clone .'", "' . $columns .'", "' . $rows . '", "' . $orientation . '", index, "' . $gridmode . '",
// Generate the two lists in HTML. "' . $itemheight . '", "' . $itemwidth . '");' . "\n";
$code .= 'stackSortable.add_headers(headers);' . "\n"; // Generate the two lists, headers and index in HTML.
$code .= 'stackSortable.add_reorientation_button();' . "\n";
$code .= 'stackSortable.create_row_col_divs();' . "\n";
$code .= 'if (index !== undefined) {stackSortable.add_index(index);};' . "\n";
$code .= 'stackSortable.add_headers(headers, available_header);' . "\n";
$code .= 'stackSortable.generate_used();' . "\n"; $code .= 'stackSortable.generate_used();' . "\n";
$code .= 'stackSortable.generate_available();' . "\n"; $code .= 'stackSortable.generate_available();' . "\n";
// Create the Sortable objects. // Create the Sortable objects.
// First, instantiate with default options first in order to extract all possible options for validation. // First, instantiate with default options first in order to extract all possible options for validation.
$code .= 'var sortableUsed = Sortable.create(usedList);' . "\n"; $code .= 'var sortableUsed =
$code .= 'var possibleOptionKeys = Object.keys(sortableUsed.options).concat(SUPPORTED_CALLBACK_FUNCTIONS);' . "\n"; stackSortable.ids.used.map((idList) =>
idList.map((usedId) => Sortable.create(document.getElementById(usedId))));' . "\n";
$code .= 'var possibleOptionKeys = Object.keys(sortableUsed[0][0].options).concat(SUPPORTED_CALLBACK_FUNCTIONS);' . "\n";
// Now set appropriate options. // Now set appropriate options.
$code .= 'Object.entries(stackSortable.options.used).forEach(([key, val]) => sortableUsed.option(key, val));' . "\n";
$code .= 'sortableUsed.forEach((sortableList) =>
sortableList.forEach((sortable) =>
Object.entries(stackSortable.options.used).forEach(
([key, val]) => sortable.option(key, val))));' . "\n";
$code .= 'var sortableAvailable = Sortable.create(availableList, stackSortable.options.available);' . "\n"; $code .= 'var sortableAvailable = Sortable.create(availableList, stackSortable.options.available);' . "\n";
// Add the onSort option in order to link up to input and overwrite user onSort if passed. // Add the onSort option in order to link up to input and overwrite user onSort if passed.
$code .= 'sortableUsed.option("onSort", () => {stackSortable.update_state(sortableUsed, sortableAvailable);});' . "\n"; $code .= 'sortableUsed.forEach((sortableList) =>
sortableList.forEach((sortable) =>
sortable.option("onSort", () => {
stackSortable.update_state(sortableUsed, sortableAvailable);})
)
);' . "\n";
$code .= 'sortableAvailable.option("onSort", () => {stackSortable.update_state(sortableUsed, sortableAvailable);});' . "\n"; $code .= 'sortableAvailable.option("onSort", () => {stackSortable.update_state(sortableUsed, sortableAvailable);});' . "\n";
// Options can now be validated since sortable objects have been instantiated, we throw warnings only. // Options can now be validated since sortable objects have been instantiated, we throw warnings only.
...@@ -241,8 +329,10 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -241,8 +329,10 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
$code .= 'stackSortable.add_delete_all_listener("delete-all", sortableUsed, sortableAvailable);' . "\n"; $code .= 'stackSortable.add_delete_all_listener("delete-all", sortableUsed, sortableAvailable);' . "\n";
} }
// Add double-click events. // Add double-click events for proof.
if ($proofmode) {
$code .= 'stackSortable.add_dblclick_listeners(sortableUsed, sortableAvailable);' . "\n"; $code .= 'stackSortable.add_dblclick_listeners(sortableUsed, sortableAvailable);' . "\n";
}
// Typeset MathJax. MathJax 2 uses Queue, whereas 3 works with promises. // Typeset MathJax. MathJax 2 uses Queue, whereas 3 works with promises.
$code .= ($mathjaxversion === "2") ? $code .= ($mathjaxversion === "2") ?
...@@ -292,7 +382,6 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -292,7 +382,6 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
// NOTE! List ordered by length. For the trimming logic. // NOTE! List ordered by length. For the trimming logic.
$validunits = [ $validunits = [
'vmin', 'vmax', 'rem', 'em', 'ex', 'px', 'cm', 'mm', 'vmin', 'vmax', 'rem', 'em', 'ex', 'px', 'cm', 'mm',
'in', 'pt', 'pc', 'ch', 'vh', 'vw', '%', 'in', 'pt', 'pc', 'ch', 'vh', 'vw', '%',
]; ];
...@@ -370,18 +459,66 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block { ...@@ -370,18 +459,66 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
} }
} }
// Check value of transpose is only "true" or "false"
if (array_key_exists('transpose', $this->params)) {
if (!in_array($this->params['transpose'], ['true', 'false'])) {
$valid = false;
$err[] = stack_string('stackBlock_parsons_unknown_transpose_value');
}
}
// Check value of columns is a string containing a numeric positive integer
if (array_key_exists("columns", $this->params)) {
if (!(preg_match('/^\d+$/', $this->params["columns"]) && intval($this->params["columns"]) > 0)) {
$valid = false;
$err[] = stack_string("stackBlock_parsons_invalid_columns_value");
}
}
// Check value of rows is a string containing a numeric positive integer
if (array_key_exists("rows", $this->params)) {
if (!(preg_match('/^\d+$/', $this->params["rows"]) && intval($this->params["rows"]) > 0)) {
$valid = false;
$err[] = stack_string("stackBlock_parsons_invalid_rows_value");
}
}
// Check we cannot have rows specified without columns
if (array_key_exists("rows", $this->params) && !array_key_exists("columns", $this->params)) {
$valid = false;
$err[] = stack_string("stackBlock_parsons_underdefined_grid");
}
// Check value of `item-height` is a string containing a positive integer
if (array_key_exists("item-height", $this->params)) {
if (!(preg_match('/^\d+$/', $this->params["item-height"]) && intval($this->params["item-height"]) > 0)) {
$valid = false;
$err[] = stack_string("stackBlock_parsons_invalid_item-height_value");
}
}
// Check value of `item-width` is a string containing a positive integer
if (array_key_exists("item-width", $this->params)) {
if (!(preg_match('/^\d+$/', $this->params["item-width"]) && intval($this->params["item-width"]) > 0)) {
$valid = false;
$err[] = stack_string("stackBlock_parsons_invalid_item-width_value");
}
}
// Check that only valid parameters are passed to block header. // Check that only valid parameters are passed to block header.
$valids = null; $valids = null;
foreach ($this->params as $key => $value) { foreach ($this->params as $key => $value) {
if ($key !== 'width' && $key !== 'height' && $key !== 'aspect-ratio' && if ($key !== 'width' && $key !== 'height' && $key !== 'aspect-ratio' &&
$key !== 'version' && $key !== 'overridecss' && $key !== 'input' && $key !== 'version' && $key !== 'overridecss' && $key !== 'input'
$key !== 'orientation' && $key !== 'clone') { && $key !== 'clone' && $key !== 'columns' && $key !== 'rows' &&
$key !== 'transpose' && $key !== 'item-height' && $key !== 'item-width') {
$err[] = "Unknown parameter '$key' for Parson's block."; $err[] = "Unknown parameter '$key' for Parson's block.";
$valid = false; $valid = false;
if ($valids === null) { if ($valids === null) {
$valids = [ $valids = [
'width', 'height', 'aspect-ratio', 'version', 'overridecss', 'width', 'height', 'aspect-ratio', 'version', 'overridecss',
'overridejs', 'input', 'orientation', 'clone', 'overridejs', 'input', 'clone', 'columns', 'rows', 'transpose', 'item-height',
'item-width',
]; ];
$err[] = stack_string('stackBlock_parsons_param', [ $err[] = stack_string('stackBlock_parsons_param', [
'param' => implode(', ', $valids), 'param' => implode(', ', $valids),
......
/* Author Salvatore Mercuri
University of Edinburgh
Copyright (C) 2024 Salvatore Mercuri
This program is free software: you can redistribute it or modify
it under the terms of the GNU General Public License version two.
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 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/>. */
/******************************************************************/
/* Functions for extracting data from matching problems */
/* in STACK to a format that can be assessed by the author. */
/* Should be used when providing model answers and writing */
/* PRTs for matching problems using the `parsons` block. */
/* */
/* Salvatore Mercuri, <smercuri@ed.ac.uk> */
/* V1.0 May 2024 */
/* */
/******************************************************************/
/* To use these functions load the library via one of the following
two commands inside `Question variables`.
stack_include("https://raw.githubusercontent.com/maths/moodle-qtype_stack/proof-builder/stack/maxima/contrib/matchlib.mac");
stack_include("contribl://matchlib.mac");
*/
/******************************************************************/
/* */
/* Assessment helper functions */
/* */
/******************************************************************/
/*
* Use this to extract an answer from the student's input of desirable format
* for assessing.
*
* Take the JSON from STACK Parson's block when using `columns` and/or
* `rows` header parameter, and returns a two-dimensional array corresponding to
* the answer keys in the JSON.
*
* If only `columns` has been specified in the `parsons` block, then use
* this function as `match_interpret(ans1)`. This will return an
* array of shape `(columns, ?)` if, where `?` represents variable dimension.
*
* If both `rows` and `columns` have been specified in the `parson` block, then
* use this function as `match_interpret(ans1, true)`. This will
* return an array of shape `(columns, rows)`.
*/
match_interpret(st, [rows]) := block([js, arr],
js: stackjson_parse(st),
arr: stackmap_get(js, "used"),
if rows=[] then arr:map(lambda([keys], first(keys)), arr)
else arr:map(lambda([keys], map(lambda([k], first(k)), keys)), arr),
return(arr)
);
/*
* Auxiliary function.
*
* Takes a list of matched keys and returns the keys not used.
* Needed to create a "teacher's answer" in JSON format, including unused text.
*/
match_keys_used_unused(ans, steps) := block([tkeys],
tkeys:map(first, steps),
return([ans, listdifference(tkeys, ev(unique(flatten(ans)), simp))])
);
/*
* Use this to transform the teacher's answer into the shape expected by the Parson's block.
* Returns an array of `[answer_keys, unused_keys]`, where `unused_keys` is always a flat
* list of keys that are in the question but not inside `ans`.
*
* If only `columns` has been specified in the `parsons` block, then use
* this function as `match_reshape(ans1)`. This will return `answer_keys` as an
* array of shape `(columns, 1, ?)` if, where `?` represents variable dimension.
*
* If both `rows` and `columns` have been specified in the `parson` block, then
* use this function as `match_interpret(ans1, true)`. This will
* return `answer_keys` as an array of shape `(columns, rows, 1)`.
*/
match_reshape(ans, steps, [rows]) := block([tkeys, akeys],
tkeys: match_keys_used_unused(ans, steps),
if rows=[] then akeys: map(lambda([keys], [keys]), first(tkeys))
else akeys:map(lambda([keys], map(lambda([k], [k]), keys)), first(tkeys)),
return([akeys, second(tkeys)])
);
/*
* Use this to transform the teacher's answer into the JSON format expected by the `Model answer` field.
*
* If only `columns` has been specified in the `parsons` block, then use
* this function as `match_correct(ans1)`.
*
* If both `rows` and `columns` have been specified in the `parson` block, then
* use this function as `match_correct(ans1, true)`.
*/
match_correct(ans, steps, [rows]) := block([akeys, ukeys],
if rows=[] then [akeys, ukeys]: match_reshape(ans, steps)
else [akeys, ukeys]: match_reshape(ans, steps, rows),
sconcat("{\"used\":", stackjson_stringify(akeys), ", \"available\":", stackjson_stringify(ukeys), "}")
);
/*
* Use this to turn a row-grouped answer into a column-grouped answer and vice-versa.
*
* Note that model answers for matching problems in STACK should always be written by grouping
* the columns, that is they should be a two-dimensional array of shape `(columns, rows)`. Authors
* may prefer to use the row-grouped answer in PRTs. This function will move between them.
*/
match_transpose(ans) := block(
return(args(transpose(apply(matrix, ans))))
);
/*
* Use this on both the model answer and the student input
* when you do not care about the order within a column.
*
* It will turn `[[a, b], [c, d], [e, f]]` into `[{a, b}, {c, d}, {e, f}]`.
*/
match_column_set(ans) := block(
return(map(lambda([col], apply(set, col)), ans))
);
/*
* Use this on both the model answer and the student input
* when you do not care about the order within a row.
*
* It will turn `[[a, b], [c, d], [e, f]]` into `[{a, c, e}, {b, d, f}]`.
*/
match_row_set(ans) := block(
return(match_column_set(match_transpose(ans)))
);
/*
* Use this on both the model answer and the student input
* when you do not care about the order between columns.
*
* It will turn `[[a, b], [c, d], [e, f]]` into `{[a, b], [c, d], [e, f]}`.
*/
match_set_column(ans) := block(
return(apply(set, ans))
);
/*
* Use this on both the model answer and the student input
* when you do not care about the order between rows.
*
* It will turn `[[a, b], [c, d], [e, f]]` into `{[a, c, e], [b, d, f]}`.
*/
match_set_row(ans) := block(
return(match_set_column(match_transpose(ans)))
);
...@@ -177,8 +177,9 @@ proof_remove_nullproof(ex):= block( ...@@ -177,8 +177,9 @@ proof_remove_nullproof(ex):= block(
*/ */
proof_parsons_interpret(st) := block([pf], proof_parsons_interpret(st) := block([pf],
pf:stackjson_parse(st), pf:stackjson_parse(st),
pf:apply(proof, stackmap_get(pf, "used")) pf:apply(proof, first(first(stackmap_get(pf, "used"))))
); );
s_test_case(proof_parsons_interpret("{\"used\":[[[\"0\",\"3\",\"5\"]]],\"available\":[\"1\",\"2\",\"4\",\"6\",\"7\"]}"), proof("0","3","5"));
/* /*
* Takes a proof, and proof steps list and returns the keys not used in the proof_steps. * Takes a proof, and proof steps list and returns the keys not used in the proof_steps.
...@@ -197,7 +198,7 @@ proof_parsons_key_json(proof_ans, proof_steps) := block([pkeys], ...@@ -197,7 +198,7 @@ proof_parsons_key_json(proof_ans, proof_steps) := block([pkeys],
/* Ensure all keys are string keys. */ /* Ensure all keys are string keys. */
if not(emptyp(proof_steps)) then proof_ans:proof_keys_sub(proof_ans, proof_steps), if not(emptyp(proof_steps)) then proof_ans:proof_keys_sub(proof_ans, proof_steps),
pkeys:proof_parsons_keys_used_unused(proof_ans, proof_steps), pkeys:proof_parsons_keys_used_unused(proof_ans, proof_steps),
sconcat("{\"used\":", stackjson_stringify(first(pkeys)), ", \"available\":", stackjson_stringify(second(pkeys)), "}") sconcat("{\"used\":", stackjson_stringify([[first(pkeys)]]), ", \"available\":", stackjson_stringify(second(pkeys)), "}")
); );
/******************************************************************/ /******************************************************************/
...@@ -435,8 +436,9 @@ proof_assessment_display(saa, pf) := block([st, k], ...@@ -435,8 +436,9 @@ proof_assessment_display(saa, pf) := block([st, k],
/* Turn the st list of lists into a string to display. */ /* Turn the st list of lists into a string to display. */
st:dl_disp(st), st:dl_disp(st),
for k:1 thru length(saa) do block( for k:1 thru length(saa) do block(
st[k]:proof_line_disp(proof_column_disp(first(st[k])), proof_column_disp2(second(st[k]))) st[k]:proof_line_disp(proof_column_disp(first(st[k])), proof_column_disp(second(st[k])))
), ),
st:apply(sconcat, st), st:apply(sconcat, st),
sconcat("<div class='proof'>", st, "</div>") sconcat("<div class='proof'>", st, "</div>")
); );
...@@ -283,7 +283,7 @@ class parsons_block_test extends qtype_stack_testcase { ...@@ -283,7 +283,7 @@ class parsons_block_test extends qtype_stack_testcase {
$invalidparameters = ['bad_param', 'HEIGHT', 'Height', 'override-css']; $invalidparameters = ['bad_param', 'HEIGHT', 'Height', 'override-css'];
$validparameters = [ $validparameters = [
'width', 'height', 'aspect-ratio', 'version', 'overridecss', 'width', 'height', 'aspect-ratio', 'version', 'overridecss',
'overridejs', 'input', 'orientation', 'clone', 'overridejs', 'input', 'clone', 'columns', 'rows', 'transpose', 'item-height', 'item-width'
]; ];
foreach ($invalidparameters as $param) { foreach ($invalidparameters as $param) {
......
...@@ -18,5 +18,11 @@ ...@@ -18,5 +18,11 @@
<version>As for STACK</version> <version>As for STACK</version>
<license>MIT License</license> <license>MIT License</license>
</library> </library>
<library>
<location>corsscripts/sortablecore.min.js</location>
<name>Sortable</name>
<version>1.15.0</version>
<license>MIT License</license>
</library>
</libraries> </libraries>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment