diff --git a/.DS_Store b/.DS_Store index fb62fc23..6bc27d56 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.env.example b/.env.example index 0e41d3ed..2bf5db19 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -PORT = 3000 \ No newline at end of file +NODE_ENV= +PARSE_HOST= +PARSE_PATH= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 87eb213b..cc3ff179 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules .idea/ dist/ +.vscode #sample input files for testing. Not essential to ignore #test-inputs/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 25dd8e58..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "terminal.explorerKind": "external", - "terminal.integrated.accessibleViewFocusOnCommandExecution": true -} \ No newline at end of file diff --git a/docs/Renderable_Json_Format.md b/docs/Renderable_Json_Format.md new file mode 100644 index 00000000..2c8fc1ec --- /dev/null +++ b/docs/Renderable_Json_Format.md @@ -0,0 +1,371 @@ +# Renderable JSON Format +This covers the JSON format this library expects in order to properly render Parsons Problems + +## Table of contents +- Overview +- Format / Schema +- Fields +- Examples +- Validation +- Versioning & changelog +- Authors & license + +## Overview +Explain what this format represents, intended consumers, and high-level constraints. + +## Format / Schema + +### Top Level Schema + +Example skeleton: +```json +{ + "question_text": "", + "options": {}, + "blocks": [] +} +``` + +## Fields + +### Question Text +The instructions for for the exercise + +key : `"question_text"`
+type: string + +### Options +Contains the configuration of features necessary for the proper rendering of the Parsons Problem + +key : `"options"`
+type : map + +```json +{ + "grader": { + "type": "dag", + "show_feedback": true + }, + "maxdist": 0, + "indent": { + "active":true, + "mode":"", // prescribed or free + "max_indents":3 //Defaults to 3 if not specified + }, + "adaptive": true, + "numbered": false, + "language": "math" +} +``` +- `"grader"` + - Types are `dag` for order-graded exercises and `execute` for execution-based assignments + - Show Feedback is booloean to determine whether students get feedback after checking their answer +- `"maxdist"` macimum number of distractors allowed. (Deprecated) +- `"indent"` + - `active`: Boolean. If false, the `mode` and `max_indents` should not be provided. + - `mode`: String. Options are `prescribed` and `free`. + - `free` will not allow block level indent specifications. Block level indent specifications should be ignored if present. Users should be allowed to indent blocks as they wish. Two main use cases are: exercises that allow indentation but do not rely on the indents for correctness; execution-based exercises. + - `prescribed` requires all blocks to include an `"indent"` key for specifying its correct indent level. This is especially useful for grading order-graded exercises that have indents. + - `max_indents`: Number. The number of indent levels to be provided to the user. The **default** value will be set to 3 if not specified. + +- `"adaptive"`: Boolean. Is it an adaptive parsons problem or not. This is `true` by **default** +- `"numbered"`: Boolean. Attaches numbers to the blocks when set to **true** +- `"language"`: String. Determines how the block text is displayed. It may also determine how code is extracted for execution provided the language has certain quirks. Supported options are: + - `"math'`, `"natural"` - Renders text as if it were Git Flavored Markdown and handles latex. This should be the default mode. + - `"python"`, `"java"`, `"javascript"`, `"html"`, `"c"`, `"c++"`, `"cpp"`,`"ruby"`. + + + +### Blocks +Contains the list of blocks to be displayed. + +key : `"blocks"`
+type: []blocks (list of blocks) + +Structure of a block: + +JSON skeleton (fill values as needed): +```json +{ + "text": "", + "code": "", + "type": "", + "tag": "", + "depends": "", + "indent": "", + "displaymath": "", + "feedback": "", + "toggle_options": [ + { + "start_index": "", + "end_index": "", + "values": [ "", "..."] + } + ] +} +``` + +Field reference (fill descriptions/examples): + +- text + - type: string + - required: yes + - example: "for i in range(n):" + - notes: Rendered content; may contain Git Flavored Markdown/LaTeX. + +- code + - type: string + - required: no + - example: "for i in range(n):\n print(i)" + - notes: Raw code for execution if the display text is not executable. eg Pseudocode or code in a different language. + +- type + - type: string + - required: no + - options: "distractor", "fixed", "toggle", "textbox" + - notes: Defines the type of block. + +- tag + - type: string + - required: no + - example: any unique string + - notes: Identifier used by depends to describe correct orderings + +- depends + - type: string or array[string] + - required: yes, for order-grading only + - example: "randomg1b1" or ["a","b"] + - notes: Ordering/dependency constraints (tag names). + +- indent + - type: number + - required: conditional (when `"indent.mode" = "prescribed"`) + - example: 0, 1, 2 + - notes: If top-level indent.mode == "prescribed" use a number indicating level.Should be ommited otherwise. + +- displaymath + - type: boolean + - required: no + - example: true + - notes: Whether to render as display math / Markdown. + +TODO: feedack and toggle_options should be lower level options within `type` + +- feedback + - type: string + - required: no + - example: "This line should be before the loop." + - notes: Per-block feedback shown after grading/checking. + +- toggle_options + - type: array of objects + - required: no + - example: see skeleton + - notes: Defines in-line selectable options; each object has: + - start_index (number): start position in text + - end_index (number): end position in text + - values (array[string]): allowed substitutions + + + +Add any other custom keys your renderer uses with the same mini-spec format above. + * No code selection was provided. + * Please paste the code you want documented and indicate the desired + * documentation style (e.g., Javadoc, XML doc, Python docstring). + */ + + +## Examples +Full Example 1: +```json +{ + "question_text": "

Put the blocks in the proper order.

\n", + "options": { + "grader": { + "type": "dag", + "show_feedback": true + }, + "maxdist": 0, + "indent": { + "active":true, + "mode":"", // prescribed or free + "max_indents":3 //Defaults to 3 if not specified + }, + "adaptive": true, + "numbered": false, + "language": "math", + "runnable": true + }, + "blocks": [ + { + "text": "Fixed Start", + "type": "", + "tag": "fixed", + "depends": "", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Random Group 1 Block 1", + "type": "", + "tag": "randomg1b1", + "depends": "", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Random Group 1 Block 2", + "type": "", + "tag": "randomg1b2", + "depends": "randomg1b1", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Random Group 1 $\\textbf{Block}$ 3", + "type": "", + "tag": "randomg1b3", + "depends": "randomg1b2", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Fixed $\\textbf{Middle}$", + "type": "", + "tag": "fixed", + "depends": "", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Random Group 2 Block 1", + "type": "", + "tag": "randomg2b1", + "depends": "randomg1b3", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Random Group 2 Block 2", + "type": "", + "tag": "randomg2b2", + "depends": "randomg2b1", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Random Group 2 Block 3", + "type": "", + "tag": "randomg2b3", + "depends": "randomg2b2", + "indent": "", + "displaymath": true, + "feedback": "" + }, + { + "text": "Fixed End", + "type": "", + "tag": "fixed", + "depends": "", + "indent": "", + "displaymath": true, + "feedback": "" + } + ] +} +``` +
+ +Toggles Example +```json +{ + "question_text": "

The constructed code should print the minimum of variables a and b.

\n", + "options": { + "grader": { + "type": "exec", + "showFeedback": true + }, + "maxdist": 0, + "order": "", + "indent": true, + "adaptive": true, + "numbered": false, + "language": "python", + "runnable": true + }, + "blocks": [ + { + "text": "if ``a`b`c`` ``<`>`>=`` b", + "type": "", + "tag": "one1", + "depends": "", + "indent": true, + "displaymath": true, + "feedback": "", + "toggle_options": [ + { + "start_index": 3, + "end_index": 12, + "values": [ + "a", + "b", + "c" + ] + }, + { + "start_index": 13, + "end_index": 23, + "values": [ + "<", + ">", + ">=" + ] + } + ] + }, + { + "text": "print(a)", + "type": "", + "tag": "two", + "depends": "", + "indent": true, + "displaymath": true, + "feedback": "" + }, + { + "text": "else", + "type": "", + "tag": "three", + "depends": "", + "indent": true, + "displaymath": true, + "feedback": "" + }, + { + "text": "print(b)", + "type": "", + "tag": "four", + "depends": "", + "indent": true, + "displaymath": true, + "feedback": "" + } + ] +} +``` + +## Validation + + + +## Local Testing +Run the server normally. +To access files in the ./tests/postitive directory, use the endpoint route: /parsons/test/`` +For example: For an instance running on port 3000, http://localhost:3000/parsons/test/test diff --git a/downloads/fixed-demo.peml b/downloads/fixed-demo.peml index 59ded2ef..5be34202 100644 --- a/downloads/fixed-demo.peml +++ b/downloads/fixed-demo.peml @@ -23,7 +23,7 @@ Put the blocks in the proper order. # the user can place an orderable block so that things appear in a # reasonable order. -[assets.code.blocks.content] +[.assets.code.blocks.content] blockid: fixed display: Fixed Start diff --git a/package-lock.json b/package-lock.json index 712e00bd..97611ee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parsons-prakhar", - "version": "1.0.2", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "parsons-prakhar", - "version": "1.0.2", + "version": "1.0.4", "license": "ISC", "dependencies": { "css-loader": "7.1.2", diff --git a/package.json b/package.json index 67cd5b33..e2b0a829 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "README.md" ], "scripts": { - "build": "webpack", + "build": "webpack --env production", + "build:dev": "webpack --env development", + "build:standalone": "webpack --env development --env standalone", "start": "node server/index.js", - "dev": "webpack --watch & nodemon server/index.js", + "dev": "webpack --env development --watch & nodemon server/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/server/helpers/parsePIF.js b/server/helpers/parsePIF.js index ccf2a03e..431fc5f2 100644 --- a/server/helpers/parsePIF.js +++ b/server/helpers/parsePIF.js @@ -1,5 +1,6 @@ const fs = require('fs'); const https = require('https'); +const http = require('http'); const path = require('path'); const FormData = require('form-data'); const { logEvent } = require('./logger'); @@ -21,15 +22,18 @@ async function parsePIF(source, filename) { ); formBody.append('is_pif', 'true'); + const isDevEnv = process.env.NODE_ENV === 'development'; const parseCallOptions = { method: 'POST', - host: 'endeavour.cs.vt.edu', - path: '/peml-live/api/parse', + host: isDevEnv ? process.env.PARSE_HOST : 'endeavour.cs.vt.edu', + port: isDevEnv ? process.env.PARSE_PORT : undefined, + path: isDevEnv? process.env.PARSE_PATH : '/peml-live/api/parse', headers: formBody.getHeaders(), }; return new Promise((resolve, reject) => { - const req = https.request(parseCallOptions, (res) => { + const httpx = isDevEnv ? http : https; + const req = httpx.request(parseCallOptions, (res) => { let responseBody = ''; res.setEncoding('utf-8'); diff --git a/server/index.js b/server/index.js index efba63b8..3612bcfd 100644 --- a/server/index.js +++ b/server/index.js @@ -158,6 +158,7 @@ app.get('/parsons/api/files', async (req, res) => { //Parse PIF file and inject into the page to render the exercise +// DEPRECATED: Use /parsons/pifjson instead app.get('/parsons/pif/:source/:filename', async (req, res) => { const filename = req.params.filename; const source = req.params.source; @@ -316,6 +317,54 @@ app.get('/parsons/pifjson/:source/:filename', async (req, res) => { res.send(dom.serialize()); }); +//Renders a pre-parsed JSON from tests/positive/ directly, +// bypassing PEML parsing. Used for frontend tests. +app.get('/parsons/test/:name', (req, res) => { + const fixtureDir = path.join(__dirname, '../tests/positive'); + const fixturePath = path.join(fixtureDir, `${req.params.name}.json`); + + if (!fs.existsSync(fixturePath)) { + return res.status(404).send(`Fixture "${req.params.name}.json" not found in tests/positive/`); + } + + let parsedJson; + try { + parsedJson = JSON.parse(fs.readFileSync(fixturePath, 'utf8')); + } catch (e) { + return res.status(400).send(`Invalid JSON in fixture "${req.params.name}.json": ${e.message}`); + } + + const dom = new JSDOM(parsonsPageTemplate); + const window = dom.window; + const $ = jqueryFactory(window); + + $('body').append(` +
+
+

${parsedJson.value.question_text || 'Please arrange the code blocks correctly.'}

+
+
+ + `); + + res.send(dom.serialize()); +}); + // Home page - list available files and upload option app.get('/parsons/', async (req, res) => { try { diff --git a/server/renderer.js b/server/renderer.js index 944fd699..b1b86e77 100644 --- a/server/renderer.js +++ b/server/renderer.js @@ -126,8 +126,7 @@ const parsonsPageTemplate = ` - - +