Our new breadboard activities all support authoring the entire content from an online authoring system.
The authoring database can be found here: [http://couchdb.cosmos.concord.org/_utils/database.html?sparks]
New activity files can be created quickly, and given an id. That id then instantly becomes a url where you can find the activity you have been authoring. This is designed to be a nice, quick, iterative authoring setup.
If you give an activity the id X, it can be found at [https://sparks-activities.concord.org/sparks-content/activities.html#X]. So, for example, the activity file with the id series-interpretive, for instance, which can be found here: [http://couchdb.cosmos.concord.org/_utils/document.html?sparks/series-interpretive], will instantly create an activity which can be found at [https://sparks-activities.concord.org/sparks-content/activities.html#series-interpretive]
Note: We now can author both "Activities" and "Sections" (or "Levels"). One activity is made up of several sections. Most of the information below is about authoring sections. Authoring activities is described at the bottom of this document.
{info:title=Table of Contents} {toc} {info}
h1. Section JSON documents
Activity sections (levels) are specified using a JavaScript syntax called JSON. The complete JSON specification can be found at [http://www.json.org/], but here are the basics:
A JSON object is an object that is surrounded by curly braces {tt}{ }{tt} and contains keys and values. Each key must be a string, and each value can be either
- a primitive (A string, a number or a boolean (true/false)
- another JSON object
- an array containing any number of these three things (primitives, JSON and arrays), surrounded by square brackets {tt}[ ]{tt}.
as you can see, it can seem a little recursive, as you can have a property which is an array of arrays of JSON objects of arrays... but at the end there will always be primitives.
h2. Syntax
The syntax of JSON is simple, but is also easy to mess up.
- All strings must be surrounded by quotes. This includes property keys. If a string had quotes within it, use single quotes on the inside
- Every value or element of an array must end in a comma, except the last element. This last rule may seem strange, but it is for the same reason that we would be confused if someone defined a 3-dimensional location as {tt}(1,5,6,){tt}. It would appear to be missing the last term.
A good syntax check can be found at [http://www.jsonlint.com/]
So here we have a legal JSON document (minus the comments, which are not legal):
{code} { "key1": "value", // a string value "key2": 2, // a number "key3": { // a JSON obeject "innerkey1": "value", "innerkey2": "value" }, "key4": [ // an array of strings "array value 1", "array value 2" ], "key5": [ // an array of JSON objects { "innerkey1": "value", "innerkey2": "value" }, { "innerkey1": "value", "innerkey2": "value" // note lack of trailing commas } ] } {code}
h1. Starting a new section
To create a new section file in the authoring database, we first create the new database entry, copy-and-paste a bare-bones, syntactically-correct activity, and then add content.
Click "New Document" from the top-left of [http://couchdb.cosmos.concord.org/_utils/database.html?sparks]
Save the document. You will get an error. This is because CouchDB didn't notice you changed the id, and tried to show you a document with the original randomly-generated id. Ignore this annoyance, and return to the main index. You will see your activity.
When you open the document, you will see a new property has been added called "_rev." This is the revision number, and allows CouchDB to keep track of edits. This should not be modified
{code} // { // "_id": "my-level", "title": "My level", "show_multimeter": "false", "circuit": [ ], "pages": [ { "questions": [ { "prompt": "Question 1" }, { "prompt": "Question 2" } ] } ], // "_rev": "xyz123" // } {code}
As you type, clicking off of the document will cause it to validate itself, which is a quick way to know if your syntax is correct.
Save the document. (Save early and often!) Visit it at [https://sparks-activities.concord.org/sparks-content/activities.html#my-level]
h1. Level properties
The following is a list of all the top-level properties a level may have. Each property is described in more detail below:
|| Property || || || circuit | The circuit model used in the level || || image | An image displayed above the questions || || pages | The list of pages containing questions and notes || || hide_circuit | See Showing and hiding Flash elements || || show_multimeter | Adds a DMM to the circuit. || || show_oscilloscope | Adds an oscilloscope to the circuit. Only one of the previous two properties may be set. || || allow_move_yellow_probe | Allows the yellow probe that is normally stuck to the positive rail to be moved by the student || || disable_multimeter_position | See Showing and hiding Flash elements || || referenceFrequency | Frequency, in Hz, to be used when calculating inductance or capacitance of reactive components from a desired impedance. ||
h1. Defining the circuit
The circuit is defined as an array of components. Currently we can author resistors, capacitors, inductors, and the power source {code} "circuit": [ { // component 1 }, { // component 2 } ] {code}
Each component contains a minimum of two properties: a type and a pair of connections. The following is a list of properties that circuit components may have:
|| Component type || Property || Values || || All | type (required) | wire \ resistor \ capacitor \ inductor \ battery \ function generator || | | connections (required) | holeName1,holeName2 \ e.g. a1,b20 \ left_positive20,a23 \ left_negative5,b5 | | | label | A label that will be shown when the user mouses over the component. Max two characters? | | | UID | A unique ID for referring to this component elsewhere || || Resistors | resistance (in ohms) | If specified, the resistor will have this exact resistance. Nominal resistance (to be called rated resistance later) is calculated automatically unless otherwise specified. You can also request a random value using a 3-element array whose first element is the string "uniform". Where possible, a table of "sensible" resistor values (expanded to span the needed orders of magnitude) for the given tolerance will be used, and randomly selected from. \ \ Examples: \ {tt}1000{tt} -- resistance should by 1000 Ohm exactly. \ {tt}["uniform", 700, 100000]{tt} -- impedance should be a randomly chosen, sensible value between 700 Ohm and 100,000 Ohm. (If the {tt}tolerance{tt} is 0.05, the list of "sensible values" will be 750, 820, 910, 1000, 1110, ..., 82000, 91000 Ohm.) | | | nominalResistance | (To be called ratedResistance.) Rated resistance. If actual resistance (above) is not specified, will be randomly generated within the tolerance of the rated resistance | | | colors | An array of band colors, e.g. {tt}["red","blue","green","gold"]{tt}. Equivalent to specifying nominalResistance | | | tolerance | To be added |
| | resistance constrained by other resistors | to be added | || Capacitors | capacitance | The exact capacitance of this capacitor, in Farads. To access this value in a question script, use {tt}.getCapacitance(){tt} rather than {tt}.capacitance{tt} | | | impedance | The desired impedance of this capacitor at its {tt}referenceFrequency{tt}, in Ohms. If the {tt}capacitance{tt} property is not set on this capacitor, it will be calculated from the impedance. You can specify an exact value (as a number) or request a random value using a 3-element array whose first element is the string "uniform". \ \ Examples: \ {tt}1000{tt} -- impedance should by 1000 Ohm at the {tt}referenceFrequency{tt} \ {tt}["uniform", 100, 1000]{tt} -- impedance should be a randomly chosen value between 100 Ohm and 1000 Ohm, at the {tt}referenceFrequency{tt} | | | referenceFrequency | When specifying a desired impedance instead of an exact capacitance value, this is the frequency in Hz at which the capacitor should have that impedance. This property does not need to be explicitly set on the capacitor, as the top-level {tt}referenceFrequency{tt} will be used if not overridden by defining it here. | || Inductors | inductance | The exact inductance of this inductor, in Henries. To access this value in a question script, use {tt}.getInductance(){tt} rather than {tt}.inductance{tt} | | | impedance | The desired impedance of this inductor at its {tt}referenceFrequency{tt}, in Ohms. If the {tt}inductance{tt} property is not set on this inductor, it will be calculated from the impedance. You can specify an exact value (as a number) or request a random value using a 3-element array whose first element is the string "uniform". \ \ Examples: \ {tt}1000{tt} -- impedance should by 1000 Ohm at the {tt}referenceFrequency{tt} \ {tt}["uniform", 100, 1000]{tt} -- impedance should be a randomly chosen value between 100 Ohm and 1000 Ohm, at the {tt}referenceFrequency{tt} | | | referenceFrequency | When specifying a desired impedance instead of an exact inductance value, this is the frequency in Hz at which the inductor should have that impedance. This property does not need to be explicitly set on the inductor, as the top-level {tt}referenceFrequency{tt} will be used if not overridden by defining it here. | || Batteries | voltage (in volts) | Can be either a specific voltage, or a range. A range is specified as an array. E.g.: \ 9 \ {tt}[8.0, 9.1]{tt} -- A random voltage selected between these 8V and 9.1V \ \ Note: See the section Defining the power source in a circuit for usage | || Function generators | amplitude | The peak amplitude, in volts. Can be a plain number or an array representing a range of values. Examples \ {tt}10{tt} -- A single peak amplitude of 10V \ {tt}[0, 10]{tt} -- A range of amplitudes from 0 to 10V, initially set at 5V (halfway) \ {tt}[10, 100, 20]{tt} -- A range from 10 to 100V, initially set at 20V| | | frequencies | An array representing a set of possible frequencies that the generator can produce. Examples: \ {tt}[1000]{tt} -- A single frequency of 1KHz. Note that it must be an array \ {tt}[1000, 2000, 5000]{tt} three possible frequencies \ {tt}["linear", 1000, 10000, 5]{tt} -- A range of frequencies from 1 KHz to 10 KHz, in 5 linear increments \ {tt}["logarithmic", 1000, 1e6, 20]{tt} -- A range of frequencies from 1 KHz to 1 MHz, in 20 logarithmic increments \ \ Note: See the section Defining the power source in a circuit for usage | | | initialFrequency | Optional. The initial frequency setting of the function generator. If this value is set, the frequency generator will initially be set to that frequency, in the set of frequencies specified by {tt}frequencies{tt}, that is closest to {tt}initialFrequency{tt} |
h2. An example circuit
The following in an example circuit with three resistors in series. This example should help see how to use the list of properties defined above to create your own circuits:
{code} "circuit": [ { "type": "wire", // the type of this first component, in this case a wire "connections": "left_positive20,a23" // the holes this component connects to }, { "type": "resistor", // a second component, this time the type is "resistor" "UID": "r1", "connections": "b23,b17", "label": "R1", "resistance": 2000 // here the author is specifying the actual resistance. }, { "type": "resistor", "UID": "r2", "connections": "c17,c11", "label": "R2", "nominalResistance": 1000 // here the author has chosen to specify the nominal resistance (colors) }, { "type": "resistor", // neither resistance nor nominalResistance has been specified here, so it will be randomized "UID": "r3", "connections": "d11,d5", "label": "R3" }, { "type": "wire", "connections": "left_negative3,a5" } ] {code}
h2. Defining the power source in a circuit
An author can set the power source of a circuit to be either a DC battery or an AC function generator using the "battery" or "function generator" components defined in the components table above. In order for the application to understand that this component is being designated as the primary power source (connected to the power rails), this component must be given the UID "source".
Any circuits created without a power source with the UID "source" are given a default 9V battery as their source.
A power source is added to a circuit just like any other component. However, as it is assumed to be connected to the power rails, it does not need to specify its connections. See the components table above to see all the properties a battery or function generator may have.
Examples:
{code} "circuit": [ { "type": "battery", // adding a battery as the breadboard power source "UID": "source", // we must define the UID as "source" "voltage": [8.5, 9] // setting the voltage to be between 8.5 and 9 (random) }, { "type": "wire", "connections": "left_positive20,a23" }, { "type": "resistor", "UID": "r1", "connections": "b23,b17", "label": "R1", "resistance": 2000 }, { "type": "wire", "connections": "left_negative3,a5" } ] {code} {code} "circuit": [ { "type": "function generator", // adding an AC function generator as the breadboard power source "UID": "source", "amplitude": 10, // 10V peak amplitude "frequencies": [1000] // the frequencies array with only a single value specified }, { "type": "wire", "connections": "left_positive20,a23" }, { "type": "resistor", "UID": "r1", "connections": "b23,b17", "label": "R1", "resistance": 2000 }, { "type": "wire", "connections": "left_negative3,a5" } ] {code}
Note: Batteries and function generators are "first-class components" which, like resistors or capacitors, could theoretically be added to the circuit anywhere by an author. This is not recommended, however, as there is no visual representation of batteries or function generators in Flash, besides the one attached to the rails.
h2. Defining faults in a circuit
SPARKS contains a very generalizable system for creating faults in a circuit. An author can
- specify specific faults for specific components
- specify specific faults for any number of randomly-selected components
- specify specific faults for any random number of components (up to an authored max)
- specify random faults for any of the above (i.e. picking a random fault for each resistor)
Currently this system is limited to creating "open" or "shorted" faults on resistors (max resistance and min (shorted) resistance respectively). However, the system is flexible enough that as we come up with new ways to break the circuit we can add them in easily to the same system.
Faults are defined in a new "faults" array, typically defined right after the circuit definition. The application always creates the circuit first, generating appropriate resistor values etc., and then applies the faults.
{code} "circuit": [...], "faults": [...], {code}
Examples in this case may be quicker to understand than a property table. The property table is below, but here are some example faults: {code} // creates an "open" fault on R1 and a "shorted" fault on R2 "faults": [ { "type": "open", "component": "r1" }, { "type": "shorted", "component": "r2" } ] {code}
{code} // creates an "open" fault on one random resistor "faults": [ { "type": "open", "count": 1 } ] {code}
{code} // creates faults on two random resistors, randomly choosing "open" or "shorted" for each "faults": [ { "type": ["open", "shorted"], "count": 2 } ] {code}
{code} // creates a shorted fault on anywhere from 1 to 5 resistors, and an // open fault on R2 "faults": [ { "type": "shorted", "max": 5 }, { "type": "open", "component": "r2" } ] {code}
As you can see, you can have multiple faults defined, and each fault can affect multiple resistors.
|| Property || Meaning || Possible values || || type | Type of fault | "open", "shorted" || || | Array of any of the above. Randomly chooses from the array independently for each component | {tt}[...]{tt} || || component | Specific component to apply this to | "r1" etc. || || count | Number of components to apply this to. Specific components will be randomly chosen | 1+ || || max | Maximum number of components to apply this to, randomly chosen between 1 and max | 1+ ||
h2. Showing and hiding Flash elements
The circuit as defined above represents the underlying Javascript model of the circuit. Typically, this will then be rendered by Flash and displayed to the student, along with a DMM. However, there are certain section-level properties that can control what the student sees:
|| Property || Meaning || Possible values, default if not specified || || hide_circuit | If true, Flash circuit will not appear | true, false (Default=false) || || show_multimeter | If true, DMM is available for the student | true, false (Default=false) || || show_oscilloscope | If true, the oscilloscope is available for the student. (Cannot be combined with the above) | true, false (Default=false) || || disable_multimeter_position | Section of the DMM to be disabled | Any one of "r,dcv,acv,dca,diode,hfe,c_10a,p_9v" (Default=none) ||
h1. Images
Images can be references in two places: at the section level, and at the question level.
{code} { "title": "My level", "image": "http://...", // this image will be at the top of every page "pages": [ { "questions": [ { "prompt": "Question 1" }, { "image": "http://...", // this image will be displayed above this question "prompt": "Question 2" } ] } ], } {code}
h2. Attaching images to the document
Images can be attached directly to the authoring document in CouchDB, and then referenced by name only. This makes it very easy to add new images.
h1. Pages
A single Sparks section consists of one circuit (or main image) and several internal "pages" of questions. Each page consists of a few questions, and as the student moves through each page within a section the main circuit will stay the same.
After a student completes a page, they immediately have their answers graded, and they see a mini-report of the questions on that page. At this point, they have the option of moving on to the next page (or section), or repeating the page again.
If they choose to repeat the page, the circuit will reload, and may contain new values for resistors or other components. This new circuit will also be used for any subsequent pages (unless the student chooses to repeat yet again).
h2. Defining pages
Pages are defined in an array, and each page is a JSON object containing questions, optional notes for the student, and optional points for time.
{code} "pages": [ { // page 1 "questions": [ { //question 1 }, { //question 2 } ], "notes": "This message will appear on this page for students" "time": { ... } // see below }, { // page 2 "questions": [ { //question 1 }, { //question 2 } ], "notes": "This message will appear on this page for students after they turn the page" } ] {code}
Note that even if you only wish to have one page of questions, it is still necessary to define the array of pages - the array would just have one page in it. An example of a one-page level can be found in the section Starting a new section above.
h2. Notes
Anything in the "notes" property of the page will show up in a box to the right of the questions (we can play with layout later if necessary). Notes can contain any plain text, HTML, and will also perform circuit calculations, to allow authors to use variables from the current circuit. For information on using calculations, please see the section Calculated answers with circuit variables below.
Example note:
{code}
"note": "The value of R1 is [${r1.nominalResistance}] ohms.
The value of R2 is [${r2.nominalResistance}] ohms."
{code}
h2. Time
An author can specify that a student should gain points for completing a page in a certain amount of time. The author can specify the "best" time to complete it in, in which case they score full points, and the "worst" time to complete it in, in which case they score zero bonus points. The points decline linearly between the two times.
Example:
{code} "time": { "best": 60, "worst": 120, "points": 5 } {code}
h1. Questions
Each page contains an array of questions. All questions (at the moment) are automatically graded as soon as a student submits their answer, and are tabulated in a report at the end of each page.
Most questions have a specific "correct" answer. In multiple-choice questions, this correct answer is defined implicitly by the score assigned to that response. For an open-response question, the correct answer must be defined explicitly in the question.
h2. Defining questions
The questions definition is an array of questions, and each question may optionally contain an array of subquestions.
The example below shows the syntax using plain open-response questions (which are simpler) for clarity.
{code} "questions": [ { "prompt": "What is the answer to question 1?", "correct_answer": "The answer" }, { "prompt": "What is the answer to", "subquestions": [ { "prompt": "question 2?", "correct_answer": "Answer 2" }, { "prompt": "question 3?", "correct_answer": "Answer 3" } ] }, { "prompt": "What is the answer to question 4?", "correct_answer": "The answer", "category": "Questions about the number 4" } ] {code}
Each "subquestion" is actually a unique question, and is graded as if it were its own question. Visually, however, a subquestion is nested under an outer prompt, and a group of subquestions has only one submit button. So the page above would look like {noformat}
- What is the answer to question 1? [________] {submit}
- What is the answer to question 2? [] question 3? [] {submit}
- What is the answer to question 4? [________] {submit} {noformat}
h3. Question properties
By default, all questions are open-response. That is, if the only thing specified is a prompt, the question will be styled with an input box after the prompt. The two bottom properties in this list are only used if no "options" are set, and will be ignored for multiple-choice questions.
|| Object || Property || Values || || Question | prompt (required) | The question being asked || | | shortPrompt | An optional summary of the prompt for use in reports. Particularly useful for subquestions, where a subquestion prompt may be nothing more than "R1?," the shortPrompt could be "Resistance of R1." | | | options | A list of options, containing the choices and the points and feedback associated with each choice. Described below. | | | radio/checkbox/ keepOrder | Only for multiple-choice questions. Described below. | | | correct_units | If specified, a units pull-down will appear. Note, only the "unit type" needs to be specified, e.g. 'V', 'A', 'ohms.' More on units below. | | | tutorial | A link to the appropriate tutorial: a button will show in the report table if the question is answered incorrectly | | | correct_answer | Only for open-response questions. If a correct answer is specified, an exact match will be scored as correct. | | | points | Only for open-response questions. Max points for this question, given to student if answer is correct | | | category | deprecated Marks the question as being a member of a category, for reporting back to the student and teacher. NOTE: Now just setting the tutorial will automatically set the category | | | show_read_multimeter_button | Adds a "read multimeter" button, instead of a text box, that reads the multimeter into the answer box. If the circuit is an AC circuit, the frequency and amplitude of the source are additionally (but invisibly) recorded as part of the answer and are available to question scripts; see below. | | | scoring | A script used to grade a question and provide feedback. See Question scoring scripts below | | | beforeScript | A script run when the question is first enabled. See Question before scripts below | | | meta | An object that may contain more information about the answer for author scripts. Not settable by author, it is instead filled when a user answers a question. See The question meta object below |
h3. Multi-choice questions
Multi-choice questions have the advantage of being easier to score and easier to provide feedback for. Since we make it possible to provide partial credit, each possible answer can have points specified. In a report, the answer is shown as being correct if it is the answer with the highest possible points.
Each option is a JSON object with the option, optional feedback for picking that option, and optional points for picking that option. || Object || Property || Values || || Question | options | An array of options || | | radio | if "true," question will shown as radio buttons. If omitted, options will be pull-down list. | | | checkbox | if "true," question will shown as check boxes (multichoice). If omitted, options will be pull-down list. | | | keepOrder | the order of the options is randomized by default. If keepOrder is true, the order will not be randomized | || Options | option | The option the student sees || | | points | Points given to the student for answering that option. Zero if omitted | | | feedback | Feedback shown to the student for answering that option. Nothing if omitted | | | tutorial | Overrides the "tutorial" specified at the question-level for this option |
Example: {code} { "prompt": "What is the answer?", "options": [ { "option": "A", "points": 5, // maximum points, so this answer is considered correct "feedback": "Good job!" }, { "option": "B", "points": 1, "feedback": "That's not quite right..." }, { "option": "C", "feedback": "Did you even read the material?", "tutorial": "finding_the_answer.html" } ] } {code}
h2. Calculated answers with circuit variables
Often you want an answer to use the values of the components in the specific circuit the user is viewing. For this purpose, we have a special calculation syntax that can be used both as a multi-choice option and as a correct_answer.
Anything in square brackets {tt}[ ]{tt} will be run through the script parser. A script can be a simple calculation. So {code} "option": "Ten is [5*2]" {code} Will be displayed as "Ten is 10." Likewise, {tt}"correct_answer": "[(5/2) * 10]"{tt} will score a question correct only if the student answers "25".
Circuit variables are accessed in the script simply by referring to the component's UID. Each component in the circuit is accessible in the script, along with all it's properties: {tt}uuid.property{tt}. Again, it must by between square brackets {tt}[ ]{tt} to be processed be the script parse. So {code} "option": "[r1.resistance]" {code} will display the resistance of r1. Similarly, "{tt}r2.nominalResistance{tt}" would give you the nominalResistance of r2. Any numerical property from the circuit component properties defined above may be used.
Math and variables can be freely mixed: variables will be converted and treated as numbers. So "{tt}[2 * r2.nominalResistance]{tt}" will give you twice the nominal resistance of r2.
The code between square brackets {tt}[ ]{tt} can, in fact, be a complete script. See more about this in the Scripting section below.
h3. Units
Adding a unit such as "V", "A" or "ohms" to the end of an option or correct_answer will immediately cause the value to be converted to engineering format.
So if you specify {code} "option": "[r1.resistance] ohms" {code} The value will be displayed in ohms if the number is between 0 - 1000, kiloohms if the number is between 10^3 - 10^6, etc.
Note: for now this is the default. If we have a reason that the author needs to show "10,000 ohms" in the dropdown box, we can make this an option. For now, if the author specifies 10000 ohms as an option, it will be automatically converted to 10 kiloohms.
h3. More math functions
The parsing code (which parses everything in an answer between square brackets {tt}[ ]{tt}) has access to the entire JavaScript library, including the JavaScript Math object: [http://www.w3schools.com/jsref/jsref_obj_math.asp]. The Math object has been extended with a couple other useful functions, {tt}Math.log10(x){tt} and {tt}Math.powNdigits(x,n){tt}.
Some other useful functions include {tt}Math.max(a,b,c...){tt} to return the maximum of n values, {tt}Math.round(a){tt} to round a value to the nearest integer, and {tt}Math.random(){tt} to get a random floating-point number between 0 and 1.
Some examples of this in use:
{code} "option": "[Math.max(r1.resistance,r2.resistance)] ohms" // if r1=100 and r2=200, this statement would resolve to "200 ohms" "option": "[Math.round(100 * Math.random())]" // returns a random integer between 0 and 100 "option": "[r1.resistance * Math.sqrt(r2.resistance)]" // returns r1 times the square root of r2 (who knows why...) "option": "[Math.ceil(Math.log10(r1.resistance))]" // the number of digits in the resistance of r1 (Math.ceil rounds up) {code}
Some math functions were added to Sparks that would be particularly helpful to authors:
{code} Math.log10(x) Math.powNdigits(x,n) -- not really sure what that does. It's equivalent to: Math.pow(10,Math.floor(Math.log(x)/Math.LN10-n+1))
// The following use the cMath (circuit Math) object for dealing with circuit variables cMath.rSeries(x,y,z,...) // will calculate the series resistance of the named resistors. So cMath.rSeries("r1","r2") will add the resistances of r1 and r2. You can have unlimited named resistors cMath.rParallel(x,y,z,...) // will calculate the parallel resistance of the named resistors. cMath.rNominalSeries(x,y,z...) cMath.rNominalParallel(x,y,z...) // will do the same for the nominal resistances cMath.vDiv(x,y) // will calculate the proportion of the voltage across resistor x, if x and y are in series. {code}
h2. Question categories
All questions can have categories assigned to them. The reports that are shown to a student will show the student the percentage of questions in these categories they have answered correctly. Unlike the regular scoring, these percentages take into account questions answered incorrectly, so if a student answers an "Understanding breadboards" question incorrectly on page 1, and then repeats the page and gets it right, the table will show them as having answered 50% of "Understanding breadboards" questions correctly.
Note that categories are just strings, and if two such string differ, even just by case, then they will be counted as two different categories.
Example:
{code} "questions": [ { "prompt": "What is the answer to question 1?", "correct_answer": "The answer", "category": "Intro questions" }, { "prompt": "What is the answer to question 2?", "correct_answer": "The answer", "category": "Hard questions" } ] {code}
h2. Question scoring scripts
All questions can use authored scripts to score answers, instead of relying on the Sparks application's inbuilt scoring system (i.e. "correct_answer" and point values for options). If a question contains a script, the script will be run when the report is generated and no other scoring or processing will be done. This means that it is up to the author to manually set the student's score, the correct answer, tutorial buttons etc, from within the script.
Scripts are written in JavaScript, and are added to the "scoring" property of a question.
A very simple script:
Here is a trivial script which an author would never use (as it could be done by other means), but should be illustrative of the script style.
{code} { "prompt": "What is 1 + 1?", "points": 10, "scoring": "if (question.answer == 2) {question.points_earned = 10}" } {code}
Here we see three things:
- The script has access to the question object, which is defined below. Furthermore, the question object has already had it's "answer" property (i.e. the student's answer) set by the system, so it can use it to score points.
- The script can set properties on the question object, such as the points_earned. This will be the score the student earns.
- If the points_earned is equal to (or greater than) the point-value of the question (set above the script), the question will be considered correct.
Access to the circuit
Along with the question object, the script also has access to the circuit, using the same r1, r2 variables defined earlier. Using this, we can modify our question above:
{code} { "prompt": "What is the rated resistance of R1?", "points": 10, "scoring": "if (question.answer == r1.nominalResistance) {question.points_earned = 10}" } {code}
Again, this isn't a script that an author would probably write, as there are simpler ways of scoring this simple question.
Finally, multiple-choice questions can be treated exactly the same way as the open-response questions above. question.answer will simply be set to whatever answer they picked (as a string). Using this, we can create a very simple script for a basic faulty circuit question:
{code} { "prompt": "One of these resistors is faulty and is allowing no current through it. Using the fewest number of measurements, can you work out which it is?", "points": "10", "options": [ { "option": "R1" }, { "option": "R2" }, { "option": "R3" }, { "option": "R4" } ], "keepOrder": true, "scoring": "if (question.answer.toLowerCase() === breadboard.getFault().UID) {question.points_earned = 10} question.correct_answer = breadboard.getFault().UID" } {code}
the script is, unfortunately, all on one line. To make it easier to follow, I will reproduce it with more typical line spacing:
{code} if (question.answer.toLowerCase() === breadboard.getFault().UID) { // check if the student got the answer right question.points_earned = 10 // if so, award full marks }
question.correct_answer = breadboard.getFault().UID // set correct_answer, so that this shows up in the student's report {code}
Here, we create a variable called badResistorName. We then set this variable to "R1" if the bad resistor is r1, etc. Finally, we check to see if the student's answer was the badResistorName, and, if so, award full points. (If we do not set points_earned, it will be zero. In this case, if question.answer is not equals to badResistorName, no points will be earned.) We also set question.correct_answer to be the badResistorName.
Of course, there were numerous ways to do this. Would also have made a more complicated if-statement: {tt}if (question.answer == "R1" && ${r1.resistance} > 1e12) { question.points_earned = 10 } else if ....{tt}.
h3. The question object
The question object in the Sparks application contains all the information needed to display a question and make a report: the prompt, the point-value, the correct_option (sometimes), and, after the question has been graded, the points earned, feedback, tutorials to be displayed and so on. You can set any property you want, which is, of course, dangerous: if you set question.answer, for instance, you will be changing the student's actual answer.
The following are the properties that might be relevant to a script author:
|| Property || Meaning || || answer | The answer the student made - either the open response they typed in or the choice they selected, or the multimeter reading. If the multimeter reading was made in an AC circuit, {tt}answer{tt} will be an object with properties {tt}reading{tt}, {tt}frequency{tt}, and {tt}amplitude{tt}; see below. || || points | The maximum points this question is worth || || points_earned | The points the student scored for this question. If equal to 'points', the question is marked correct || || feedback | The feedback to be displayed (won't show if the question is answered correctly) || || tutorial | The link to the tutorial button || || meta | An object containing additional information. See below ||
h4. The question meta object
Whenever a student answers a question, additional information may be saved in the question.meta object, such as the state of the circuit when the user hit "submit".
|| Property || Meaning || || question.meta.frequency | The frequency of the power source when the user hit submit (AC only) || || question.meta.amplitude | The amplitude of the power source when the user hit submit (AC only) || || question.meta.dmmDial | The current DMM dial setting as a string || || question.meta.oscopeScaleQuality | The "quality" of the current OScope scale, from 0 to 1 || || question.meta.val | Used for multimeter-button readings (below) || || question.meta.units | Used for multimeter-button readings (below)||
h4. Using the multimeter button
If the question used the "Read Multimeter" button (by setting the property {tt}show_read_multimeter_button{tt} of the question object to true), the user will be shown a button allowing them to directly enter the value from the multimeter. The question's answer property will be set to a string representing what was seen on the DMM (e.g. "20 mA"). However, just like parsing an answer (see Parsing written answers), the question will also save two additional properties in the meta object: question.meta.val will be the actual value of the reading at the base SI unit, and question.meta.units will be the units.
So if a student presses "Read Multimeter" when the DMM is showing "20 mA", the following properties will be saved: || Property || Value given input of "20 mA" || || question.answer | "20 mA" || || question.meta.val | 0.02 || || question.meta.units | "A" ||
h3. The log object
Scripts have access to the student log, and can get the current log through the helper variable log. The following methods are available:
|| Method || || || measurements() | total number of measurements (inc. dial spinning) || || uniqueVMeasurements() | unique voltage measurements || || uniqueIMeasurements() | ... current || || uniqueRMeasurements() | ... resistance || || connectionBreaks() | number of times student broke a connection (lifted a lead) || || connectionMakes() | replaced a lead || || blownFuses() | number of times student blew the fuse ||
h3. Parsing written answers
Two in-built script functions are available to help authors decipher and score hand-written answers, "parse" and "close".
parse(question.answer) will take the student's answer and return an object. That's object's val is the parsed value, and units is the parsed units.
close(num1, num3, optionalPercent) will return true if num1 is close to num2. By default, closeness is considered to be within 5%. You can optionally put in a third parameter, so close(100, 120, 20) will return true, because 120 is within 20% of 100.
Example:
{code} var parsedAnswer = parse(question.answer); var valueCorrect = close(parsedAnswer.val, 5010); if (valueCorrect) { question.points_earned = 10; } {code}
h3. Other useful functions for scripts
Besides the functions "parse" and "close" described above, there are a number of other specialized functions that may be useful to script authors.
|| Area || Function || What it does || Example || || Parsing, measurements and units | sparks.unit.convertMeasurement(string) | Converts a measurement (as a string) to engineering notation if possible | ...convertMeasurement("0.0045 A") -> "4.5 mA" | || | sparks.unit.toEngineering(value, units) | Converts a measurement (as a value and a string unit) to an object in engineering notation | ...toEngineering(0.0045, "A") -> {tt}{value: 4.5, units: "mA"}{tt} | || Math | See the section More math functions above |
h2. Question before scripts
The "beforeScript" in a question is run the moment that question is enabled. So for the first question on a page, it is run immediately when the user flips to that page, and for later questions on the page it is run when the user hits "Submit" on the question above.
You can do anything in these scripts, but two common actions are showing or hiding the DMM or OScope:
{code} { "prompt": "Look, now the DMM has appeared! What is the rated resistance of R1?", "beforeScript": "sparks.sectionController.setDMMVisibility(true)", }, { "prompt": "Awww... now the OScope has gone!", "beforeScript": "sparks.sectionController.setOScopeVisibility(false)", } {code}
h1. General scripting tips and strategies
h2. Global variables
All scripts can set global variables using the sparks.vars.X, where x is any variable name.
Note the order that scripts are executed in: All question options are read first, one after the other, and any scripts executed. Then the Notes is read and any scripts executed. Then, after the student has subitted all the answers on a page, the scoring scripts are executed. If a variable is defined in a late-executed script, it can't be used in an earlier script.
Example:
{code} "questions": [ { "prompt": "What is the resistance of R1?", "correct_answer": "[sparks.vars.a = 10/2; sparks.vars.a]", "correct_units": "ohms" }, { "prompt": "What is the resistance of R1?", "correct_answer": "[sparks.vars.a]", "correct_units": "ohms" } ] {code}
h2. Finding faults
Quickly finding circuit faults is possible in any script.
|| Method || || || breadboard.getFault() | returns the 1st faulty resistor in the circuit, or only fault is there is only one || || breadboard.getFaults() | returns the array of all the faults. breadboard.getFaults()[0] gives you the first (as above); breadboard.getFaults()[1] gives you the second, etc.; breadboard.getFaults().length gives you the total number of faults. ||
the resistor objects returned by the functions above are exactly the same as r1, r2 above. So you can say {tt}breadboard.getFault().UID{tt} for the id, {tt}breadboard.getFault().resistance{tt}, etc.
You can now check if a resistor is open or shorted quickly. {tt}r1.open{tt} and {tt}r1.shorted{tt} return true or false. Likewise, {tt}breadboard.getFault().open{tt}, or {tt}breadboard.getFaults()[1].shorted{tt} will tell you what type of fault the known-bad-resistors 1 and 2 are.
h2. Logging to the console
From within any script, you can log messages to the Javascript console (visible on Chrome through View -> Developer -> Javascript Console) using {tt}console.log(X){tt}. Not only can you use this to log variable values (e.g. {tt}console.log("The res of R1 is " + r1.resistance){tt}), but if you log an object alone it will display itself in the console in such a way that you can open it up:
|| console.log(r1) | Print (and open up) the object representing R1 || || console.log(log) | Print the current log ||
h1. Defining an activity
Sparks activities are made up of multiple sections (levels). A given section can be used in multiple activities.
To define an activity, you can create a new JSON document in the Couch Database, and give it three simple properties: type: activity, to distinguish it from sections, a title, and sections, an array of the ids of the sections you want to use:
{code} { "_id": "series-resistances", "type": "activity", "title": "Series Resistances", "sections": [ "series-a", "series-b", "series-c", "series-d", "series-e", "series-f" ] } {code}
Just as with levels, ff you give an activity the id X, it can be found at [https://sparks-activities.concord.org/sparks-content/activities.html#X].