diff --git a/package.json b/package.json
index 9cb61c894f..752e304362 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"packages/obonode/obojobo-chunks-iframe",
"packages/obonode/obojobo-chunks-list",
"packages/obonode/obojobo-chunks-materia",
+ "packages/obonode/obojobo-chunks-materia-assessment",
"packages/obonode/obojobo-chunks-math-equation",
"packages/obonode/obojobo-chunks-multiple-choice-assessment",
"packages/obonode/obojobo-chunks-numeric-assessment",
diff --git a/packages/app/obojobo-document-engine/src/scss/_includes.scss b/packages/app/obojobo-document-engine/src/scss/_includes.scss
index 4ff4afa9cf..3d2fca859e 100644
--- a/packages/app/obojobo-document-engine/src/scss/_includes.scss
+++ b/packages/app/obojobo-document-engine/src/scss/_includes.scss
@@ -15,6 +15,7 @@ $color-bg2: #f4f4f4;
$color-bg3: #131313;
$color-action-bg: #f9f4ff;
$color-correct: #38ae24;
+$color-partially-correct: #ffc802;
$color-incorrect: #c21f00;
$color-survey: #48b8b9;
$color-alt-correct: #ffc802;
diff --git a/packages/app/obojobo-document-json-parser/src/node-parser.js b/packages/app/obojobo-document-json-parser/src/node-parser.js
index d524702c9d..492e7018d6 100644
--- a/packages/app/obojobo-document-json-parser/src/node-parser.js
+++ b/packages/app/obojobo-document-json-parser/src/node-parser.js
@@ -11,6 +11,7 @@ const parserMap = new Map()
.set('ObojoboDraft.Chunks.IFrame', require('./parsers/iframe-node-parser'))
.set('ObojoboDraft.Chunks.List', require('./parsers/list-node-parser'))
.set('ObojoboDraft.Chunks.Materia', require('./parsers/materia-node-parser'))
+ .set('ObojoboDraft.Chunks.MateriaAssessment', require('./parsers/materia-assessment-node-parser'))
.set('ObojoboDraft.Chunks.MathEquation', require('./parsers/math-equation-node-parser'))
.set('ObojoboDraft.Chunks.MCAssessment.MCAnswer', require('./parsers/mc-answer-node-parser'))
.set('ObojoboDraft.Chunks.MCAssessment.MCChoice', require('./parsers/mc-choice-node-parser'))
diff --git a/packages/app/obojobo-document-json-parser/src/parsers/materia-assessment-node-parser.js b/packages/app/obojobo-document-json-parser/src/parsers/materia-assessment-node-parser.js
new file mode 100644
index 0000000000..39339a86f9
--- /dev/null
+++ b/packages/app/obojobo-document-json-parser/src/parsers/materia-assessment-node-parser.js
@@ -0,0 +1,17 @@
+const processAttrs = require('../process-attrs')
+const processTriggers = require('../process-triggers')
+
+const materiaAssessmentNodeParser = (node, childrenParser) => {
+ const id = node.id ? ` id="${node.id}"` : ''
+ const attrs = processAttrs(node.content, ['triggers', 'actions'])
+ const triggersXML = processTriggers(node.content.triggers)
+
+ return (
+ `` +
+ childrenParser(node.children) +
+ triggersXML +
+ ``
+ )
+}
+
+module.exports = materiaAssessmentNodeParser
diff --git a/packages/app/obojobo-document-xml-parser/src/name-transformer.js b/packages/app/obojobo-document-xml-parser/src/name-transformer.js
index c884cd20b0..a0edcd72c6 100644
--- a/packages/app/obojobo-document-xml-parser/src/name-transformer.js
+++ b/packages/app/obojobo-document-xml-parser/src/name-transformer.js
@@ -13,6 +13,7 @@ const nameMap = new Map()
.set('IFrame', 'ObojoboDraft.Chunks.IFrame')
.set('List', 'ObojoboDraft.Chunks.List')
.set('Materia', 'ObojoboDraft.Chunks.Materia')
+ .set('MateriaAssessment', 'ObojoboDraft.Chunks.MateriaAssessment')
.set('MathEquation', 'ObojoboDraft.Chunks.MathEquation')
.set('MCAnswer', 'ObojoboDraft.Chunks.MCAssessment.MCAnswer')
.set('MCAssessment', 'ObojoboDraft.Chunks.MCAssessment')
diff --git a/packages/app/obojobo-express/server/migrations/20230926141756-create-external-tool-data.js b/packages/app/obojobo-express/server/migrations/20230926141756-create-external-tool-data.js
new file mode 100644
index 0000000000..5141450538
--- /dev/null
+++ b/packages/app/obojobo-express/server/migrations/20230926141756-create-external-tool-data.js
@@ -0,0 +1,54 @@
+'use strict'
+
+let dbm
+let type
+let seed
+
+/**
+ * We receive the dbmigrate dependency from dbmigrate initially.
+ * This enables us to not have to rely on NODE_PATH.
+ */
+exports.setup = function(options, seedLink) {
+ dbm = options.dbmigrate
+ type = dbm.dataType
+ seed = seedLink
+}
+
+exports.up = function(db) {
+ return db
+ .createTable('external_tool_data', {
+ id: { type: 'bigserial', primaryKey: true },
+ created_at: {
+ type: 'timestamp WITH TIME ZONE',
+ notNull: true,
+ defaultValue: new String('now()')
+ },
+ payload: { type: 'jsonb' },
+ user_id: { type: 'bigint', notNull: true },
+ visit_id: { type: 'UUID', notNull: true },
+ draft_content_id: { type: 'UUID', notNull: true },
+ node_id: { type: 'UUID', notNull: true }
+ })
+ .then(result => {
+ return db.addIndex('external_tool_data', 'external_tool_data_user_id_index', ['user_id'])
+ })
+ .then(result => {
+ return db.addIndex('external_tool_data', 'external_tool_data_visit_id_index', ['visit_id'])
+ })
+ .then(result => {
+ return db.addIndex('external_tool_data', 'external_tool_data_draft_content_id_index', [
+ 'draft_content_id'
+ ])
+ })
+ .then(result => {
+ return db.addIndex('external_tool_data', 'external_tool_data_node_id_index', ['node_id'])
+ })
+}
+
+exports.down = function(db) {
+ return db.dropTable('external_tool_data')
+}
+
+exports._meta = {
+ version: 1
+}
diff --git a/packages/obonode/obojobo-chunks-abstract-assessment/constants.js b/packages/obonode/obojobo-chunks-abstract-assessment/constants.js
index 845ce5a5a2..65d20fe2aa 100644
--- a/packages/obonode/obojobo-chunks-abstract-assessment/constants.js
+++ b/packages/obonode/obojobo-chunks-abstract-assessment/constants.js
@@ -1,5 +1,6 @@
const CHOICE_NODE = 'ObojoboDraft.Chunks.AbstractAssessment.Choice'
const FEEDBACK_NODE = 'ObojoboDraft.Chunks.AbstractAssessment.Feedback'
+const MATERIA_ASSESSMENT_NODE = 'ObojoboDraft.Chunks.MateriaAssessment'
// Whenever an inheriting component is created
// Add its Assessment type to the valid assessments, its answer type to valid answers
@@ -16,22 +17,27 @@ import {
} from 'obojobo-chunks-multiple-choice-assessment/constants'
import mcAssessment from 'obojobo-chunks-multiple-choice-assessment/empty-node.json'
+import materiaAssessment from 'obojobo-chunks-materia-assessment/empty-node.json'
+
const assessmentToAnswer = {
[NUMERIC_ASSESSMENT_NODE]: numericAssessment.children[0].children[0],
- [MC_ASSESSMENT_NODE]: mcAssessment.children[0].children[0]
+ [MC_ASSESSMENT_NODE]: mcAssessment.children[0].children[0],
+ [MATERIA_ASSESSMENT_NODE]: materiaAssessment.children[0]
}
const answerTypeToJson = {
[NUMERIC_ANSWER_NODE]: numericAssessment.children[0].children[0],
- [MC_ANSWER_NODE]: mcAssessment.children[0].children[0]
+ [MC_ANSWER_NODE]: mcAssessment.children[0].children[0],
+ [MATERIA_ASSESSMENT_NODE]: materiaAssessment.children[0]
}
const answerToAssessment = {
[NUMERIC_ANSWER_NODE]: numericAssessment,
- [MC_ANSWER_NODE]: mcAssessment
+ [MC_ANSWER_NODE]: mcAssessment,
+ [MATERIA_ASSESSMENT_NODE]: materiaAssessment
}
-const validAssessments = [NUMERIC_ASSESSMENT_NODE, MC_ASSESSMENT_NODE]
+const validAssessments = [NUMERIC_ASSESSMENT_NODE, MC_ASSESSMENT_NODE, MATERIA_ASSESSMENT_NODE]
const validAnswers = [NUMERIC_ANSWER_NODE, MC_ANSWER_NODE]
export {
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.eslintignore b/packages/obonode/obojobo-chunks-materia-assessment/.eslintignore
new file mode 100644
index 0000000000..ba2a97b57a
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.eslintignore
@@ -0,0 +1,2 @@
+node_modules
+coverage
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.eslintrc b/packages/obonode/obojobo-chunks-materia-assessment/.eslintrc
new file mode 100644
index 0000000000..48a4b976f7
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "eslint-config-obojobo"
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.gitignore b/packages/obonode/obojobo-chunks-materia-assessment/.gitignore
new file mode 100644
index 0000000000..62562b74a3
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.gitignore
@@ -0,0 +1,2 @@
+coverage
+node_modules
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.npmignore b/packages/obonode/obojobo-chunks-materia-assessment/.npmignore
new file mode 100644
index 0000000000..7fba505d30
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.npmignore
@@ -0,0 +1,18 @@
+coverage
+node_modules
+npm-debug.log
+yarn-error.log
+*.DS_Store
+__mocks__
+__tests__
+.vscode
+.eslintignore
+.eslintrc
+.prettierignore
+.stylelintignore
+.stylelintrc
+.scss-lint.yml
+prettier.config.js
+Dockerfile
+__snapshots__
+*.test.js
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.prettierignore b/packages/obonode/obojobo-chunks-materia-assessment/.prettierignore
new file mode 100644
index 0000000000..13ab9f8412
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.prettierignore
@@ -0,0 +1,2 @@
+package.json
+coverage
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.stylelintignore b/packages/obonode/obojobo-chunks-materia-assessment/.stylelintignore
new file mode 100644
index 0000000000..2a63845aec
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.stylelintignore
@@ -0,0 +1,5 @@
+node_modules
+coverage/
+__snapshots__
+*.js
+*.jsx
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/.stylelintrc b/packages/obonode/obojobo-chunks-materia-assessment/.stylelintrc
new file mode 100644
index 0000000000..f873b6900c
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/.stylelintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "stylelint-config-obojobo"
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/__snapshots__/viewer-component.test.js.snap b/packages/obonode/obojobo-chunks-materia-assessment/__snapshots__/viewer-component.test.js.snap
new file mode 100644
index 0000000000..7f923685ee
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/__snapshots__/viewer-component.test.js.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MateriaAssessment viewer component renders 1`] = `
+
+
+ MockClassComponent Props:
+ {
+ "model": {
+ "id": "mock-uuid",
+ "content": {
+ "src": "http://www.example.com"
+ },
+ "metadata": {},
+ "index": 0,
+ "type": "ObojoboDraft.Chunks.MateriaAssessment",
+ "children": null
+ },
+ "moduleData": {
+ "model": {
+ "title": "mocked-module-title"
+ },
+ "navState": {
+ "visitId": "mock-visit-id"
+ }
+ },
+ "title": "Materia Widget"
+}
+
+
+
+ MockClassComponent Props:
+ {
+ "parentModel": {
+ "id": "mock-uuid",
+ "content": {
+ "src": "http://www.example.com"
+ },
+ "metadata": {},
+ "index": 0,
+ "type": "ObojoboDraft.Chunks.MateriaAssessment",
+ "children": null
+ },
+ "textItem": {
+ "text": {
+ "styleList": {
+ "styles": []
+ },
+ "value": ""
+ },
+ "data": {},
+ "parent": {
+ "maxItems": 1,
+ "items": [
+ null
+ ],
+ "dataTemplate": {}
+ }
+ },
+ "groupIndex": "0"
+}
+
+
+
+`;
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/babel.config.js b/packages/obonode/obojobo-chunks-materia-assessment/babel.config.js
new file mode 100644
index 0000000000..4f4b24d9bb
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/babel.config.js
@@ -0,0 +1,11 @@
+module.exports = function(api) {
+ api.cache(true)
+ return {
+ presets: ['@babel/preset-env', '@babel/preset-react'],
+ env: {
+ test: {
+ presets: ['@babel/preset-env', '@babel/preset-react']
+ }
+ }
+ }
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/converter.js b/packages/obonode/obojobo-chunks-materia-assessment/converter.js
new file mode 100644
index 0000000000..4ecef498d6
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/converter.js
@@ -0,0 +1,3 @@
+const materiaChunkConverter = require('obojobo-chunks-materia/converter')
+
+export default materiaChunkConverter
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/editor-registration.js b/packages/obonode/obojobo-chunks-materia-assessment/editor-registration.js
new file mode 100644
index 0000000000..9d12f2d105
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/editor-registration.js
@@ -0,0 +1,16 @@
+import MateriaRegistration from 'obojobo-chunks-materia/editor-registration'
+
+import emptyNode from './empty-node.json'
+
+const MATERIA_ASSESSMENT_NODE = 'ObojoboDraft.Chunks.MateriaAssessment'
+
+const MateriaAssessment = {
+ ...MateriaRegistration,
+ name: MATERIA_ASSESSMENT_NODE,
+ menuLabel: 'Materia Widget Assessment',
+ json: {
+ emptyNode
+ }
+}
+
+export default MateriaAssessment
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/editor-registration.test.js b/packages/obonode/obojobo-chunks-materia-assessment/editor-registration.test.js
new file mode 100644
index 0000000000..99326e2366
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/editor-registration.test.js
@@ -0,0 +1,43 @@
+jest.mock('obojobo-chunks-materia/editor-component', () =>
+ global.mockReactComponent(this, 'MockMateria')
+)
+jest.mock('slate')
+jest.mock('obojobo-chunks-materia/converter')
+
+import MateriaAssessment from './editor-registration'
+
+describe('Materia editorRegistration', () => {
+ test('has expected properties', () => {
+ // The editor registration for the MateriaAssessment chunk basically extends the
+ // editor registration for the Materia chunk, so it should look the same
+ expect(MateriaAssessment).toMatchInlineSnapshot(`
+ Object {
+ "helpers": Object {
+ "oboToSlate": [MockFunction],
+ "slateToObo": [MockFunction],
+ },
+ "icon": Object {},
+ "isContent": true,
+ "isInsertable": true,
+ "json": Object {
+ "emptyNode": Object {
+ "children": Array [
+ Object {
+ "text": "",
+ },
+ ],
+ "content": Object {},
+ "type": "ObojoboDraft.Chunks.MateriaAssessment",
+ },
+ },
+ "menuLabel": "Materia Widget Assessment",
+ "name": "ObojoboDraft.Chunks.MateriaAssessment",
+ "plugins": Object {
+ "decorate": [Function],
+ "onKeyDown": [Function],
+ "renderNode": [Function],
+ },
+ }
+ `)
+ })
+})
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/editor.js b/packages/obonode/obojobo-chunks-materia-assessment/editor.js
new file mode 100644
index 0000000000..386c16afe4
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/editor.js
@@ -0,0 +1,6 @@
+// Main entrypoint for editor
+import Common from 'obojobo-document-engine/src/scripts/common'
+import EditorNode from './editor-registration'
+
+// register
+Common.Registry.registerEditorModel(EditorNode)
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/editor.test.js b/packages/obonode/obojobo-chunks-materia-assessment/editor.test.js
new file mode 100644
index 0000000000..513a98a68a
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/editor.test.js
@@ -0,0 +1,24 @@
+jest.mock('obojobo-document-engine/src/scripts/common/index', () => ({
+ Registry: {
+ registerEditorModel: jest.fn()
+ }
+}))
+
+jest.mock('./editor-registration', () => ({ EditorNode: 1 }))
+
+import Common from 'obojobo-document-engine/src/scripts/common/index'
+
+describe('Question editor script', () => {
+ test('registers node', () => {
+ // shouldn't have been called yet
+ expect(Common.Registry.registerEditorModel).toHaveBeenCalledTimes(0)
+
+ require('./editor')
+ const EditorRegistration = require('./editor-registration')
+
+ // the editor script should have registered the model
+ expect(Common.Registry.registerEditorModel).toHaveBeenCalledTimes(1)
+
+ expect(Common.Registry.registerEditorModel).toHaveBeenCalledWith(EditorRegistration)
+ })
+})
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/empty-node.json b/packages/obonode/obojobo-chunks-materia-assessment/empty-node.json
new file mode 100644
index 0000000000..21cfd0cc49
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/empty-node.json
@@ -0,0 +1,5 @@
+{
+ "type":"ObojoboDraft.Chunks.MateriaAssessment",
+ "content": { },
+ "children": [{ "text": "" }]
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/index.js b/packages/obonode/obojobo-chunks-materia-assessment/index.js
new file mode 100644
index 0000000000..e3a8fbf704
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/index.js
@@ -0,0 +1,11 @@
+module.exports = {
+ obojobo: {
+ isOptional: true,
+ expressMiddleware: 'server/index.js',
+ clientScripts: {
+ viewer: 'viewer.js',
+ editor: 'editor.js'
+ },
+ serverScripts: ['server/materiaassessment']
+ }
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/package.json b/packages/obonode/obojobo-chunks-materia-assessment/package.json
new file mode 100644
index 0000000000..8d02e521bb
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "obojobo-chunks-materia-assessment",
+ "version": "13.2.1",
+ "license": "AGPL-3.0-only",
+ "description": "MateriaAssessment chunk for Obojobo",
+ "scripts": {
+ "test": "TZ='America/New_York' jest --verbose",
+ "test:ci": "TZ='America/New_York' CI=true jest --ci --useStderr --coverage --coverageReporters text-summary cobertura",
+ "lint": "yarn lint:js && yarn lint:css",
+ "lint:js": "eslint .",
+ "lint:css": "stylelint **/*.scss",
+ "prettier:run": "prettier --write '**/*.{js,scss}'",
+ "precommit": "lint-staged"
+ },
+ "lint-staged": {
+ "**/*.scss": [
+ "stylelint"
+ ],
+ "**/*.{js,scss}": [
+ "prettier --write",
+ "git add"
+ ]
+ },
+ "prettier": {
+ "printWidth": 100,
+ "semi": false,
+ "useTabs": true,
+ "singleQuote": true
+ },
+ "peerDependencies": {
+ "obojobo-chunks-question": "^13.2.1",
+ "obojobo-chunks-materia": "^13.2.1",
+ "obojobo-lib-utils": "^13.2.1"
+ },
+ "devDependencies": {
+ "lint-staged": "^10.2.2"
+ },
+ "jest": {
+ "testMatch": [
+ "**/*.test.js"
+ ],
+ "setupFilesAfterEnv": [
+ "obojobo-lib-utils/test-setup-chunks.js"
+ ],
+ "verbose": false,
+ "coverageReporters": [
+ "text",
+ "lcov"
+ ],
+ "coveragePathIgnorePatterns": [
+ "node_modules"
+ ],
+ "moduleNameMapper": {
+ "^Common(.*)$": "obojobo-document-engine/src/scripts/common$1",
+ "^Viewer(.*)$": "obojobo-document-engine/src/scripts/viewer$1",
+ "\\.(svg|scss)$": "obojobo-lib-utils/__mocks__/ignore-file-stub.js"
+ },
+ "coverageThreshold": {
+ "global": {
+ "branches": 100,
+ "functions": 100,
+ "lines": 100,
+ "statements": 100
+ }
+ }
+ }
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/server/index.js b/packages/obonode/obojobo-chunks-materia-assessment/server/index.js
new file mode 100644
index 0000000000..017d06db53
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/server/index.js
@@ -0,0 +1,46 @@
+const router = require('express').Router() //eslint-disable-line new-cap
+const db = require('obojobo-express/server/db')
+const oboEvents = require('obojobo-express/server/obo_events')
+
+const {
+ requireCurrentUser,
+ requireCurrentVisit
+} = require('obojobo-express/server/express_validators')
+
+oboEvents.on('materia:ltiScorePassback', event => {
+ db.none(
+ `INSERT INTO external_tool_data
+ (payload, user_id, visit_id, draft_content_id, node_id)
+ VALUES ($[payload], $[userId], $[visitId], $[contentId], $[nodeId])`,
+ event
+ )
+})
+
+router
+ .route('/materia-lti-score-verify')
+ .get([requireCurrentUser, requireCurrentVisit])
+ .get(async (req, res) => {
+ await db
+ .one(
+ `SELECT payload
+ FROM external_tool_data
+ WHERE user_id = $[userId]
+ AND draft_content_id = $[draftContentId]
+ AND node_id = $[nodeId]
+ ORDER BY created_at DESC
+ LIMIT 1`,
+ {
+ userId: req.currentUser.id,
+ draftContentId: req.currentVisit.draft_content_id,
+ nodeId: req.query.nodeId
+ }
+ )
+ .then(result => {
+ res.send({
+ score: result.payload.score,
+ success: result.payload.success
+ })
+ })
+ })
+
+module.exports = router
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/server/materiaassessment.js b/packages/obonode/obojobo-chunks-materia-assessment/server/materiaassessment.js
new file mode 100644
index 0000000000..2f0e052542
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/server/materiaassessment.js
@@ -0,0 +1,22 @@
+const DraftNode = require('obojobo-express/server/models/draft_node')
+
+class MateriaAssessmentNodeAssessment extends DraftNode {
+ static get nodeName() {
+ return 'ObojoboDraft.Chunks.MateriaAssessment'
+ }
+
+ constructor(draftTree, node, initFn) {
+ super(draftTree, node, initFn)
+ this.registerEvents({
+ 'ObojoboDraft.Chunks.Question:calculateScore': this.onCalculateScore
+ })
+ }
+
+ onCalculateScore(app, question, responseRecord, setScore) {
+ if (!question.contains(this.node)) return
+
+ setScore(responseRecord.response.score)
+ }
+}
+
+module.exports = MateriaAssessmentNodeAssessment
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.jsx b/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.jsx
new file mode 100644
index 0000000000..49e6ba77df
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.jsx
@@ -0,0 +1,90 @@
+import React from 'react'
+
+import API from 'obojobo-document-engine/src/scripts/viewer/util/api'
+
+import Viewer from 'obojobo-document-engine/src/scripts/viewer'
+const { OboQuestionAssessmentComponent } = Viewer.components
+
+import Materia from 'obojobo-chunks-materia/viewer-component'
+
+import './viewer-component.scss'
+
+export default class MateriaAssessment extends OboQuestionAssessmentComponent {
+ constructor(props) {
+ super(props)
+
+ this.handleScorePassback = this.handleScorePassback.bind(this)
+
+ this.inputRef = React.createRef()
+
+ this.state = {
+ score: null,
+ verifiedScore: null,
+ scoreUrl: null
+ }
+ }
+
+ static isResponseEmpty(response) {
+ return !response.verifiedScore
+ }
+
+ getInstructions() {
+ return (
+
+ Embedded Materia widget.
+ Play the embedded Materia widget to receive a score. Your highest score will be saved.
+
+ )
+ }
+
+ calculateScore() {
+ if (!this.state.score) {
+ return null
+ }
+
+ return {
+ score: this.state.score,
+ details: {
+ scoreUrl: this.state.scoreUrl
+ }
+ }
+ }
+
+ handleScorePassback(event, data) {
+ // this should probably be abstracted in a util function somewhere
+ return API.get(
+ `/materia-lti-score-verify?visitId=${this.props.moduleData.navState.visitId}&nodeId=${this.props.model.id}`,
+ 'json'
+ )
+ .then(API.processJsonResults)
+ .then(result => {
+ this.setState({
+ score: result.score,
+ verifiedScore: true,
+ scoreUrl: data.score_url
+ })
+
+ this.props.onSaveAnswer(null)
+ })
+ }
+
+ handleFormChange() {
+ return {
+ state: this.state,
+ targetId: null,
+ sendResponseImmediately: true
+ }
+ }
+
+
+ render() {
+ return (
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.scss b/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.scss
new file mode 100644
index 0000000000..1e04d71965
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.scss
@@ -0,0 +1,8 @@
+.hidden {
+ display: block;
+ padding: 0;
+ margin: 0;
+ opacity: 0;
+ height: 0;
+ width: 0;
+}
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.test.js b/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.test.js
new file mode 100644
index 0000000000..0ffc1a5a8b
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/viewer-component.test.js
@@ -0,0 +1,248 @@
+jest.mock('obojobo-document-engine/src/scripts/viewer/util/api', () => ({
+ get: jest.fn(),
+ processJsonResults: jest.fn()
+}))
+
+jest.mock('obojobo-document-engine/src/scripts/viewer', () => ({
+ components: {
+ OboQuestionAssessmentComponent: jest.requireActual(
+ 'obojobo-document-engine/src/scripts/viewer/components/obo-question-assessment-component'
+ ).default
+ },
+ util: {
+ NavUtil: {
+ getContext: jest.fn().mockReturnValue('mock:module:context')
+ }
+ }
+}))
+
+jest.mock('react-dom')
+jest.mock(
+ 'obojobo-chunks-iframe/viewer-component',
+ () => require('obojobo-document-engine/__mocks__/mock-class-component').default
+)
+jest.mock(
+ 'obojobo-document-engine/src/scripts/common/chunk/text-chunk/text-group-el',
+ () => require('obojobo-document-engine/__mocks__/mock-class-component').default
+)
+
+import Materia from 'obojobo-chunks-materia/viewer-component'
+
+import MateriaAssessment from './viewer-component'
+import OboModel from 'obojobo-document-engine/__mocks__/obo-model-mock'
+import React from 'react'
+import renderer from 'react-test-renderer'
+
+require('./viewer') // used to register this oboModel
+
+describe('MateriaAssessment viewer component', () => {
+ let model
+ let moduleData
+ let questionModel
+ let onSaveAnswer
+
+ const API = require('obojobo-document-engine/src/scripts/viewer/util/api')
+
+ const Viewer = require('obojobo-document-engine/src/scripts/viewer')
+ const { NavUtil } = Viewer.util
+
+ const mockModuleTitle = 'mocked-module-title'
+ const mockVisitId = 'mock-visit-id'
+ const mockNodeId = 'mock-obo-id'
+ const mockQuestionId = 'mock-question-id'
+
+ beforeEach(() => {
+ jest.resetAllMocks()
+ jest.spyOn(window, 'addEventListener')
+ jest.spyOn(window, 'removeEventListener')
+
+ OboModel.__setNextGeneratedLocalId('mock-uuid')
+ model = OboModel.create({
+ id: mockNodeId,
+ type: 'ObojoboDraft.Chunks.MateriaAssessment',
+ content: {
+ src: 'http://www.example.com'
+ }
+ })
+
+ moduleData = {
+ model: {
+ title: mockModuleTitle
+ },
+ navState: {
+ visitId: mockVisitId
+ }
+ }
+
+ questionModel = {
+ get: jest.fn().mockReturnValue(mockQuestionId)
+ }
+
+ onSaveAnswer = jest.fn()
+ })
+
+ afterEach(() => {})
+
+ test('renders', () => {
+ expect.hasAssertions()
+ const props = {
+ model,
+ moduleData
+ }
+ const component = renderer.create()
+ expect(component.toJSON()).toMatchSnapshot()
+
+ expect(component.root.children.length).toBe(1)
+ expect(component.root.children[0].type).toBe(Materia)
+ })
+
+ test('handleScorePassback makes an API call to verify the score', () => {
+ API.get = jest.fn().mockResolvedValue(true)
+ API.processJsonResults = jest.fn().mockResolvedValue({ score: 100, success: true })
+
+ NavUtil.getContext = jest.fn().mockReturnValue('mock:module:context')
+
+ expect.hasAssertions()
+ const props = {
+ model,
+ moduleData,
+ questionModel,
+ onSaveAnswer
+ }
+
+ const component = renderer.create()
+ const inst = component.getInstance()
+
+ const mockData = {
+ type: 'materiaScoreRecorded',
+ score: 100,
+ score_url: 'http://localhost/score'
+ }
+ const mockEvent = {
+ origin: 'http://localhost',
+ source: 'http://localhost/whatever',
+ data: JSON.stringify(mockData)
+ }
+
+ return inst.handleScorePassback(mockEvent, mockData).then(() => {
+ expect(API.get).toHaveBeenCalledTimes(1)
+ expect(API.get).toHaveBeenCalledWith(
+ `/materia-lti-score-verify?visitId=${mockVisitId}&nodeId=${mockNodeId}`,
+ 'json'
+ )
+
+ expect(inst.state).toHaveProperty('score', 100)
+ expect(onSaveAnswer).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ test('isResponseEmpty returns true if the response score is not verified - does not exist', () => {
+ expect(MateriaAssessment.isResponseEmpty({})).toBe(true)
+ expect(MateriaAssessment.isResponseEmpty({ score: 100, verifiedScore: false })).toBe(true)
+ })
+ test('isResponseEmpty returns true if the response score is not verified - false value', () => {
+ expect(MateriaAssessment.isResponseEmpty({ score: 100, verifiedScore: false })).toBe(true)
+ })
+ test('isResponseEmpty returns false if the response score is verified', () => {
+ expect(MateriaAssessment.isResponseEmpty({ score: 100, verifiedScore: true })).toBe(false)
+ })
+
+ test('calculateScore returns null when no score is recorded', () => {
+ const props = {
+ model,
+ moduleData
+ }
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+ expect(inst.calculateScore()).toBe(null)
+ })
+
+ test('calculateScore returns properly when a score is recorded', () => {
+ const mockScore = 90
+ const mockScoreUrl = 'url:to.score/screen'
+
+ const props = {
+ model,
+ moduleData
+ }
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+
+ // Ordinarily we would never set this manually, it would be a product of the score verification
+ // But we're only really checking this one function's reactions, so it should be fine.
+ inst.state = {
+ score: mockScore,
+ verifiedScore: true,
+ scoreUrl: mockScoreUrl
+ }
+
+ expect(inst.calculateScore()).toEqual({ score: mockScore, details: { scoreUrl: mockScoreUrl } })
+ })
+
+ test('getInstructions returns properly', () => {
+ const props = {
+ model,
+ moduleData
+ }
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+ const instructionsFragment = renderer.create(inst.getInstructions())
+
+ const fragmentRender = instructionsFragment.root.findByProps({
+ className: 'for-screen-reader-only'
+ })
+ expect(fragmentRender.children[0]).toBe('Embedded Materia widget.')
+ expect(fragmentRender.parent.children[1]).toBe(
+ 'Play the embedded Materia widget to receive a score. Your highest score will be saved.'
+ )
+ })
+
+ test('handleFormChange returns the expected object', () => {
+ let mockScore = 90
+ let mockScoreUrl = 'url:to.score/screen'
+
+ const props = {
+ model,
+ moduleData
+ }
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+
+ let mockNewState = {
+ score: mockScore,
+ verifiedScore: true,
+ scoreUrl: mockScoreUrl
+ }
+
+ // Ordinarily we would never set this manually, it would be a product of the score verification
+ // But we're only really checking this one function's reactions, so it should be fine.
+ inst.state = mockNewState
+
+ expect(inst.handleFormChange()).toEqual({
+ state: mockNewState,
+ targetId: null,
+ sendResponseImmediately: true
+ })
+
+ mockScore = 100
+ mockScoreUrl = 'url:to.some/other/score/screen'
+
+ mockNewState = {
+ score: mockScore,
+ verifiedScore: true,
+ scoreUrl: mockScoreUrl
+ }
+
+ inst.state = mockNewState
+
+ expect(inst.handleFormChange()).toEqual({
+ state: mockNewState,
+ targetId: null,
+ sendResponseImmediately: true
+ })
+ })
+})
diff --git a/packages/obonode/obojobo-chunks-materia-assessment/viewer.js b/packages/obonode/obojobo-chunks-materia-assessment/viewer.js
new file mode 100644
index 0000000000..7f3aac4978
--- /dev/null
+++ b/packages/obonode/obojobo-chunks-materia-assessment/viewer.js
@@ -0,0 +1,9 @@
+import adapter from 'obojobo-chunks-materia/adapter'
+import Common from 'obojobo-document-engine/src/scripts/common'
+import ViewerComponent from './viewer-component'
+
+Common.Registry.registerModel('ObojoboDraft.Chunks.MateriaAssessment', {
+ adapter: adapter,
+ componentClass: ViewerComponent,
+ type: 'chunk'
+})
diff --git a/packages/obonode/obojobo-chunks-materia/__snapshots__/adapter.test.js.snap b/packages/obonode/obojobo-chunks-materia/__snapshots__/adapter.test.js.snap
index 7a8f26961e..25846dedae 100644
--- a/packages/obonode/obojobo-chunks-materia/__snapshots__/adapter.test.js.snap
+++ b/packages/obonode/obojobo-chunks-materia/__snapshots__/adapter.test.js.snap
@@ -4,6 +4,7 @@ exports[`Materia adapter construct builds without attributes 1`] = `
Object {
"height": 600,
"icon": null,
+ "noFooter": true,
"src": null,
"textGroup": TextGroup {
"dataTemplate": Object {},
@@ -31,6 +32,7 @@ Object {
"content": Object {
"height": 600,
"icon": null,
+ "noFooter": true,
"src": "mockSrc",
"textGroup": Array [
Object {
diff --git a/packages/obonode/obojobo-chunks-materia/adapter.js b/packages/obonode/obojobo-chunks-materia/adapter.js
index 11910c5804..39dcb15f6f 100644
--- a/packages/obonode/obojobo-chunks-materia/adapter.js
+++ b/packages/obonode/obojobo-chunks-materia/adapter.js
@@ -6,7 +6,7 @@ const MIN_DIMENSION = 100
const DEFAULT_WIDTH = 800
const DEFAULT_HEIGHT = 600
-const propsList = ['height', 'icon', 'src', 'widgetEngine', 'width']
+const propsList = ['height', 'icon', 'noFooter', 'src', 'widgetEngine', 'width']
export default {
construct(model, attrs) {
@@ -26,6 +26,7 @@ export default {
)
model.setStateProp('widgetEngine', null)
model.setStateProp('icon', null)
+ model.setStateProp('noFooter', true)
},
clone(model, clone) {
diff --git a/packages/obonode/obojobo-chunks-materia/adapter.test.js b/packages/obonode/obojobo-chunks-materia/adapter.test.js
index 06c6d326c9..215d1a34e8 100644
--- a/packages/obonode/obojobo-chunks-materia/adapter.test.js
+++ b/packages/obonode/obojobo-chunks-materia/adapter.test.js
@@ -44,6 +44,12 @@ describe('Materia adapter', () => {
expect(model.modelState.widgetEngine).toBe('')
})
+ test('adapter sets noFooter to true', () => {
+ model = new MockOboModel({})
+ MateriaAdapter.construct(model)
+ expect(model.modelState.noFooter).toBe(true)
+ })
+
test('adapter sets modelState.icon to a number if specified and null otherwise', () => {
model = new MockOboModel({})
MateriaAdapter.construct(model)
diff --git a/packages/obonode/obojobo-chunks-materia/server/materia-event.js b/packages/obonode/obojobo-chunks-materia/server/materia-event.js
index d7a44e01fd..e740f1c6a5 100644
--- a/packages/obonode/obojobo-chunks-materia/server/materia-event.js
+++ b/packages/obonode/obojobo-chunks-materia/server/materia-event.js
@@ -1,5 +1,7 @@
const insertEvent = require('obojobo-express/server/insert_event')
const logger = require('obojobo-express/server/logger')
+const oboEvents = require('obojobo-express/server/obo_events')
+const { expandLisResultSourcedId } = require('./route-helpers')
// EVENTS
const insertLtiLaunchWidgetEvent = ({
@@ -93,9 +95,19 @@ const insertLtiScorePassbackEvent = ({
metadata: {},
draftId,
contentId
- }).catch(err => {
- logger.error(`There was an error inserting the ${action} event`, err)
})
+ .then(() => {
+ const payload = {
+ score
+ }
+
+ const { nodeId } = expandLisResultSourcedId(lisResultSourcedId)
+
+ oboEvents.emit(action, { userId, visitId, contentId, nodeId, payload })
+ })
+ .catch(err => {
+ logger.error(`There was an error inserting the ${action} event`, err)
+ })
}
module.exports = {
diff --git a/packages/obonode/obojobo-chunks-materia/server/route-helpers.js b/packages/obonode/obojobo-chunks-materia/server/route-helpers.js
index a2a418e3b6..63002446c2 100644
--- a/packages/obonode/obojobo-chunks-materia/server/route-helpers.js
+++ b/packages/obonode/obojobo-chunks-materia/server/route-helpers.js
@@ -58,7 +58,7 @@ const widgetLaunchParams = (document, visit, user, materiaOboNodeId, baseUrl) =>
}
// materia currently uses context_id to group scores and attempts
- // obojobo doesn't support materia as scoreable questions yet, so the key in use here is intended to:
+ // the key in use here is intended to:
// * support materia in content pages
// * re lti launch will reset scores/attempts
// * browser reload of the window will resume an attempt/score window
diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.js b/packages/obonode/obojobo-chunks-materia/viewer-component.js
index 5a1236d99d..96725a91c3 100644
--- a/packages/obonode/obojobo-chunks-materia/viewer-component.js
+++ b/packages/obonode/obojobo-chunks-materia/viewer-component.js
@@ -10,13 +10,19 @@ import IFrameSizingTypes from 'obojobo-chunks-iframe/iframe-sizing-types'
export default class Materia extends React.Component {
constructor(props) {
super(props)
+
this.iframeRef = React.createRef()
+ let iframeSrc = this.srcToLTILaunchUrl(props.moduleData.navState.visitId, props.model.id)
+ if (props.mode === 'review') {
+ iframeSrc = props.response.scoreUrl
+ }
+
// manipulate iframe settings
const model = props.model.clone()
model.modelState = {
...model.modelState,
- src: this.srcToLTILaunchUrl(props.moduleData.navState.visitId, props.model.id),
+ src: iframeSrc,
border: true,
fit: 'scale',
initialZoom: 1,
@@ -27,8 +33,8 @@ export default class Materia extends React.Component {
// state setup
this.state = {
model,
- score: null,
- verifiedScore: false,
+ visitId: props.moduleData.navState.visitId,
+ nodeId: props.model.id,
open: false
}
@@ -38,9 +44,17 @@ export default class Materia extends React.Component {
}
onPostMessageFromMateria(event) {
- // iframe isn't present OR
- // postmessage didn't come from the iframe we're listening to
- if (!this.iframeRef.current || event.source !== this.iframeRef.current.contentWindow) {
+ // no callback registered to do anything with a score event
+ if (!this.props.handleScorePassback) {
+ return
+ }
+
+ // iframe isn't present
+ if (!this.iframeRef || !this.iframeRef.current || !this.iframeRef.current.refs.iframe) {
+ return
+ }
+ // OR postmessage didn't come from the iframe we're listening to
+ if (event.source !== this.iframeRef.current.refs.iframe.contentWindow) {
return
}
@@ -58,7 +72,7 @@ export default class Materia extends React.Component {
switch (data.type) {
case 'materiaScoreRecorded':
- this.setState({ score: data.score })
+ this.props.handleScorePassback(event, data)
break
}
} catch (e) {
@@ -82,21 +96,35 @@ export default class Materia extends React.Component {
this.setState({ open: true })
}
- renderTextCaption() {
- return this.state.model.modelState.textGroup.first.text ? (
-
-
-
- ) : null
+ renderCaptionAndScore() {
+ let textCaptionRender = null
+
+ let scoreRender = null
+ if (this.props.score && this.props.verifiedScore) {
+ scoreRender = (
+ Your highest score: {this.props.score}%
+ )
+ }
+
+ if (this.state.model.modelState.textGroup.first.text) {
+ textCaptionRender = (
+
+
+ {scoreRender}
+
+ )
+ }
+
+ return textCaptionRender
}
- renderCaptionOrScore() {
+ renderTextCaption() {
try {
- return this.renderTextCaption()
+ return this.renderCaptionAndScore()
} catch (e) {
console.error('Error building Materia Caption') // eslint-disable-line no-console
return null
@@ -113,7 +141,7 @@ export default class Materia extends React.Component {
title={`${this.state.model.modelState.widgetEngine || 'Materia'} Widget`}
onShow={this.onShow}
/>
- {this.renderCaptionOrScore()}
+ {this.renderTextCaption()}
)
}
diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.scss b/packages/obonode/obojobo-chunks-materia/viewer-component.scss
index 01fc7ba159..715c739252 100644
--- a/packages/obonode/obojobo-chunks-materia/viewer-component.scss
+++ b/packages/obonode/obojobo-chunks-materia/viewer-component.scss
@@ -9,12 +9,15 @@
.materia-score {
display: block;
text-align: center;
- margin: 0 auto;
- margin-bottom: 2em;
+ margin: -5em auto 0;
font-size: 0.7em;
- opacity: 0.5;
- margin-top: -3.5em;
+ opacity: 1;
z-index: 2;
+ transition: opacity 0.4s;
+
+ &.is-not-verified {
+ opacity: 0;
+ }
}
.label {
diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.test.js b/packages/obonode/obojobo-chunks-materia/viewer-component.test.js
index 943e0a7e72..d6972d207b 100644
--- a/packages/obonode/obojobo-chunks-materia/viewer-component.test.js
+++ b/packages/obonode/obojobo-chunks-materia/viewer-component.test.js
@@ -18,6 +18,18 @@ require('./viewer') // used to register this oboModel
describe('Materia viewer component', () => {
let model
let moduleData
+ let handleScorePassback
+
+ let standardProps
+
+ const mockModuleTitle = 'mocked-module-title'
+ const mockVisitId = 'mock-visit-id'
+ const mockNodeId = 'mock-obo-id'
+
+ const standardPostMessageContent = {
+ score: 100,
+ score_url: 'url:to/score.screen'
+ }
beforeEach(() => {
jest.resetAllMocks()
@@ -26,7 +38,7 @@ describe('Materia viewer component', () => {
OboModel.__setNextGeneratedLocalId('mock-uuid')
model = OboModel.create({
- id: 'mock-obo-id',
+ id: mockNodeId,
type: 'ObojoboDraft.Chunks.Materia',
content: {
src: 'http://www.example.com'
@@ -35,32 +47,39 @@ describe('Materia viewer component', () => {
moduleData = {
model: {
- title: 'mocked-module-title'
+ title: mockModuleTitle
},
navState: {
- visitId: 'mock-visit-id'
+ visitId: mockVisitId
}
}
+
+ handleScorePassback = jest.fn()
+
+ standardProps = {
+ model,
+ moduleData,
+ handleScorePassback
+ }
})
afterEach(() => {})
test('renders', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
expect(component.toJSON()).toMatchSnapshot()
+
+ const inst = component.getInstance()
+ expect(inst.state.model.modelState.src).toBe(
+ `http://localhost/materia-lti-launch?visitId=${mockVisitId}&nodeId=${mockNodeId}`
+ )
})
test('adds and removes listener for postmessage when mounting and unmounting', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
@@ -86,145 +105,172 @@ describe('Materia viewer component', () => {
)
})
- test('onPostMessageFromMateria without an iframe ref doesnt update state', () => {
+ test('onPostMessageFromMateria does nothing if post message handler callback is not defined', () => {
expect.hasAssertions()
const props = {
- model,
- moduleData
+ ...standardProps
}
+ delete props.handleScorePassback
const component = renderer.create()
const inst = component.getInstance()
inst.iframeRef = {}
- const event = { source: '', data: JSON.stringify({ score: 100 }) }
+ const event = { source: '', data: JSON.stringify(standardPostMessageContent) }
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
})
- test('onPostMessageFromMateria without a matching iframe and event source doesnt update state', () => {
+ test('onPostMessageFromMateria with an empty iframe ref does not call post message handler callback', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'different-mock-window' } }
- const event = { source: 'mock-window', data: JSON.stringify({ score: 100 }) }
+ inst.iframeRef = {}
+ const event = { source: '', data: JSON.stringify(standardPostMessageContent) }
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
+ })
+
+ test('onPostMessageFromMateria without an iframe ref object does not call post message handler callback', () => {
+ expect.hasAssertions()
+ const props = standardProps
+
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+ inst.iframeRef = null
+ const event = { source: '', data: JSON.stringify(standardPostMessageContent) }
+ inst.onPostMessageFromMateria(event)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
+ })
+
+ test('onPostMessageFromMateria without a matching iframe and event source does not call post message handler callback', () => {
+ expect.hasAssertions()
+ const props = standardProps
+
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+ inst.iframeRef = { current: { refs: { iframe: { contentWindow: 'different-mock-window' } } } }
+ const event = { source: 'mock-window', data: JSON.stringify(standardPostMessageContent) }
+ inst.onPostMessageFromMateria(event)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
})
test('onPostMessageFromMateria blocks events not coming fom the src domain', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'http://not-localhost/' } }
- const event = { source: 'http://localhost/whatever', data: JSON.stringify({ score: 100 }) }
+ inst.iframeRef = {
+ current: { refs: { iframe: { contentWindow: 'http://not-localhost/whatever' } } }
+ }
+ const event = {
+ source: 'http://localhost/whatever',
+ data: JSON.stringify(standardPostMessageContent)
+ }
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
})
test('onPostMessageFromMateria blocks events with an origin that doesnt match modelState srcn', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } }
+ inst.iframeRef = {
+ current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } }
+ }
const event = {
origin: 'http://not-localhost',
source: 'http://localhost/whatever',
- data: JSON.stringify({ score: 100 })
+ data: JSON.stringify(standardPostMessageContent)
}
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
})
test('onPostMessageFromMateria ignores messages where data is not a string', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } }
+ inst.iframeRef = {
+ current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } }
+ }
const event = {
origin: 'http://localhost',
source: 'http://localhost/whatever',
- data: { score: 100 }
+ data: standardPostMessageContent
}
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
})
test('onPostMessageFromMateria ignores messages with a data type that isnt materiaScoreRecorded', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } }
+ inst.iframeRef = {
+ current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } }
+ }
const event = {
origin: 'http://localhost',
source: 'http://localhost/whatever',
data: JSON.stringify({ type: 'notmateriaScoreRecorded', score: 100 })
}
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
})
- test('onPostMessageFromMateria updates the score', () => {
+ test('onPostMessageFromMateria calls post message handler when all checks pass', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
-
+ const props = standardProps
const component = renderer.create()
+ // While we're here, make sure the score dialog appears correctly.
+ // It shouldn't exist by default
+ expect(component.root.findAllByProps({ className: 'materia-score verified' }).length).toBe(0)
+
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } }
+ inst.iframeRef = {
+ current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } }
+ }
const event = {
origin: 'http://localhost',
source: 'http://localhost/whatever',
- data: JSON.stringify({ type: 'materiaScoreRecorded', score: 100 })
+ data: JSON.stringify({
+ type: 'materiaScoreRecorded',
+ score: 100,
+ score_url: 'http://localhost/score'
+ })
}
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', 100)
+ expect(handleScorePassback).toHaveBeenCalledTimes(1)
})
test('onPostMessageFromMateria handles json parsing errors', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
- inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } }
+ inst.iframeRef = {
+ current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } }
+ }
const event = {
origin: 'http://localhost',
source: 'http://localhost/whatever',
@@ -233,16 +279,13 @@ describe('Materia viewer component', () => {
jest.spyOn(console, 'error')
console.error.mockReturnValueOnce() // eslint-disable-line no-console
inst.onPostMessageFromMateria(event)
- expect(inst.state).toHaveProperty('score', null)
+ expect(handleScorePassback).toHaveBeenCalledTimes(0)
expect(console.error).toHaveBeenCalled() // eslint-disable-line no-console
})
test('srcToLTILaunchUrl formats strings as expected', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
@@ -256,10 +299,7 @@ describe('Materia viewer component', () => {
test('onShow changes state to open', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
const inst = component.getInstance()
@@ -268,11 +308,24 @@ describe('Materia viewer component', () => {
expect(inst.state).toHaveProperty('open', true)
})
- test('renderCaptionOrScore renders text', () => {
+ test('renderCaptionOrScore renders text but no score when score is not set', () => {
+ expect.hasAssertions()
+ const props = standardProps
+
+ const component = renderer.create()
+
+ // find the textGroupEL by a unique prop
+ // if it's found, it was rendered
+ expect(component.root.findAllByProps({ groupIndex: '0' })).toHaveLength(1)
+ expect(component.root.findAllByProps({ className: 'materia-score verified' }).length).toBe(0)
+ })
+
+ test('renderCaptionOrScore renders text and score when score is set', () => {
expect.hasAssertions()
const props = {
- model,
- moduleData
+ ...standardProps,
+ score: 100,
+ verifiedScore: true
}
const component = renderer.create()
@@ -280,14 +333,13 @@ describe('Materia viewer component', () => {
// find the textGroupEL by a unique prop
// if it's found, it was rendered
expect(component.root.findAllByProps({ groupIndex: '0' })).toHaveLength(1)
+ const scoreRender = component.root.findByProps({ className: 'materia-score verified' })
+ expect(scoreRender.children.join('')).toBe('Your highest score: 100%')
})
test('renderCaptionOrScore captures errors', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
@@ -304,10 +356,7 @@ describe('Materia viewer component', () => {
test('renderTextCaption renders null when there is no textgroup text', () => {
expect.hasAssertions()
- const props = {
- model,
- moduleData
- }
+ const props = standardProps
const component = renderer.create()
@@ -321,4 +370,21 @@ describe('Materia viewer component', () => {
// it shouldn't render one
expect(component.root.findAllByProps({ groupIndex: '0' })).toHaveLength(0)
})
+
+ test('rendering in review mode changes the iframe src', () => {
+ const mockScoreUrl = 'http://localhost/url/to/score/screen'
+ const props = {
+ model,
+ moduleData,
+ mode: 'review',
+ response: {
+ scoreUrl: mockScoreUrl
+ }
+ }
+ const component = renderer.create()
+
+ const inst = component.getInstance()
+
+ expect(inst.state.model.modelState.src).toBe(mockScoreUrl)
+ })
})
diff --git a/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap b/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap
index 31f0557f5c..6836ed7cab 100644
--- a/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap
+++ b/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap
@@ -16,6 +16,11 @@ Object {
"z",
],
"needsUpdate": false,
+ "partialLabels": Array [
+ "l",
+ "m",
+ "n",
+ ],
"revealAnswer": "when-incorrect",
"solution": Object {
"children": Array [
@@ -60,6 +65,7 @@ Object {
"editing": false,
"incorrectLabels": null,
"needsUpdate": false,
+ "partialLabels": null,
"revealAnswer": "default",
"solution": null,
"type": "default",
@@ -74,6 +80,7 @@ Object {
"editing": false,
"incorrectLabels": null,
"needsUpdate": false,
+ "partialLabels": null,
"revealAnswer": "default",
"solution": null,
"type": "default",
@@ -86,6 +93,7 @@ Object {
"collapsed": false,
"correctLabels": "a|b|c",
"incorrectLabels": "d|e|f",
+ "partialLabels": null,
"revealAnswer": "default",
"solution": Object {
"children": Array [
@@ -129,6 +137,7 @@ Object {
"collapsed": false,
"correctLabels": null,
"incorrectLabels": null,
+ "partialLabels": null,
"revealAnswer": "default",
"solution": null,
"type": "default",
diff --git a/packages/obonode/obojobo-chunks-question/adapter.js b/packages/obonode/obojobo-chunks-question/adapter.js
index f6df18a786..f4e92fd155 100644
--- a/packages/obonode/obojobo-chunks-question/adapter.js
+++ b/packages/obonode/obojobo-chunks-question/adapter.js
@@ -34,6 +34,7 @@ const Adapter = {
}
model.setStateProp('correctLabels', null, p => p.split('|'))
+ model.setStateProp('partialLabels', null, p => p.split('|'))
model.setStateProp('incorrectLabels', null, p => p.split('|'))
},
@@ -42,6 +43,9 @@ const Adapter = {
clone.modelState.correctLabels = model.modelState.correctLabels
? model.modelState.correctLabels.slice(0)
: null
+ clone.modelState.partialLabels = model.modelState.partialLabels
+ ? model.modelState.partialLabels.slice(0)
+ : null
clone.modelState.incorrectLabels = model.modelState.incorrectLabels
? model.modelState.incorrectLabels.slice(0)
: null
@@ -59,6 +63,9 @@ const Adapter = {
json.content.correctLabels = model.modelState.correctLabels
? model.modelState.correctLabels.join('|')
: null
+ json.content.partialLabels = model.modelState.partialLabels
+ ? model.modelState.partialLabels.join('|')
+ : null
json.content.incorrectLabels = model.modelState.incorrectLabels
? model.modelState.incorrectLabels.join('|')
: null
diff --git a/packages/obonode/obojobo-chunks-question/adapter.test.js b/packages/obonode/obojobo-chunks-question/adapter.test.js
index 005e7bee24..6df49fda12 100644
--- a/packages/obonode/obojobo-chunks-question/adapter.test.js
+++ b/packages/obonode/obojobo-chunks-question/adapter.test.js
@@ -32,6 +32,7 @@ describe('Question adapter', () => {
content: {
type: 'default',
correctLabels: 'a|b|c',
+ partialLabels: 'l|m|n',
incorrectLabels: 'x|y|z',
revealAnswer: 'when-incorrect',
solution: {
@@ -72,6 +73,7 @@ describe('Question adapter', () => {
const attrs = {
content: {
correctLabels: 'a|b|c',
+ partialLabels: 'l|m|n',
incorrectLabels: 'd|e|f'
}
}
@@ -180,6 +182,19 @@ describe('Question adapter', () => {
expect(json).toMatchSnapshot()
})
+ test('toJSON builds a JSON representation with partial labels', () => {
+ const json = { content: {} }
+ const attrs = {
+ content: {
+ partialLabels: 'l|m|n'
+ }
+ }
+ const model = new OboModel(attrs)
+
+ QuestionAdapter.construct(model, attrs)
+ QuestionAdapter.toJSON(model, json)
+ })
+
test('Adapter grabs correctLabels and incorrectLabels from child components', () => {
const attrs = {
id: 'question',
@@ -192,6 +207,7 @@ describe('Question adapter', () => {
type: 'ObojoboDraft.Chunks.MCAssessment',
content: {
correctLabels: 'd|e|f',
+ partialLabels: 'l|m|n',
incorrectLabels: 'x|y|z'
},
children: []
@@ -212,4 +228,47 @@ describe('Question adapter', () => {
spy.mockRestore()
})
+
+ test('modelState uses correctLabels from content if MCAssessment child does not have any set', () => {
+ const attrs = {
+ id: 'question',
+ content: {
+ correctLabels: 'a|b|c'
+ },
+ children: [
+ {
+ id: 'mcassessment',
+ type: 'ObojoboDraft.Chunks.MCAssessment',
+ children: []
+ }
+ ]
+ }
+
+ const model = new OboModel(attrs)
+ QuestionAdapter.construct(model, attrs)
+
+ expect(model.modelState.correctLabels).toEqual(['a', 'b', 'c'])
+ })
+
+ test('modelState uses correctLabels from MCAssessment child if content does not have any set', () => {
+ const attrs = {
+ id: 'question',
+ content: {},
+ children: [
+ {
+ id: 'mcassessment',
+ type: 'ObojoboDraft.Chunks.MCAssessment',
+ content: {
+ correctLabels: 'd|e|f'
+ },
+ children: []
+ }
+ ]
+ }
+
+ const model = new OboModel(attrs)
+ QuestionAdapter.construct(model, attrs)
+
+ expect(model.modelState.correctLabels).toEqual(['d', 'e', 'f'])
+ })
})
diff --git a/packages/obonode/obojobo-chunks-question/converter.js b/packages/obonode/obojobo-chunks-question/converter.js
index 932b13e968..08c9990acf 100644
--- a/packages/obonode/obojobo-chunks-question/converter.js
+++ b/packages/obonode/obojobo-chunks-question/converter.js
@@ -6,6 +6,7 @@ const QUESTION_NODE = 'ObojoboDraft.Chunks.Question'
const SOLUTION_NODE = 'ObojoboDraft.Chunks.Question.Solution'
const MCASSESSMENT_NODE = 'ObojoboDraft.Chunks.MCAssessment'
const NUMERIC_ASSESSMENT_NODE = 'ObojoboDraft.Chunks.NumericAssessment'
+const MATERIA_ASSESSMENT_NODE = 'ObojoboDraft.Chunks.MateriaAssessment'
const PAGE_NODE = 'ObojoboDraft.Pages.Page'
/**
@@ -34,10 +35,8 @@ const slateToObo = node => {
break
case MCASSESSMENT_NODE:
- children.push(Common.Registry.getItemForType(child.type).slateToObo(child))
- break
-
case NUMERIC_ASSESSMENT_NODE:
+ case MATERIA_ASSESSMENT_NODE:
children.push(Common.Registry.getItemForType(child.type).slateToObo(child))
break
@@ -66,7 +65,11 @@ const oboToSlate = node => {
const clonedNode = JSON.parse(JSON.stringify(node))
clonedNode.children = clonedNode.children.map(child => {
- if (child.type === MCASSESSMENT_NODE || child.type === NUMERIC_ASSESSMENT_NODE) {
+ if (
+ child.type === MCASSESSMENT_NODE ||
+ child.type === NUMERIC_ASSESSMENT_NODE ||
+ child.type === MATERIA_ASSESSMENT_NODE
+ ) {
return Common.Registry.getItemForType(child.type).oboToSlate(child)
} else {
return Component.helpers.oboToSlate(child)
diff --git a/packages/obonode/obojobo-chunks-question/editor-component.js b/packages/obonode/obojobo-chunks-question/editor-component.js
index 8f45b50905..9c73b17f99 100644
--- a/packages/obonode/obojobo-chunks-question/editor-component.js
+++ b/packages/obonode/obojobo-chunks-question/editor-component.js
@@ -13,6 +13,7 @@ const QUESTION_NODE = 'ObojoboDraft.Chunks.Question'
const SOLUTION_NODE = 'ObojoboDraft.Chunks.Question.Solution'
const MCASSESSMENT_NODE = 'ObojoboDraft.Chunks.MCAssessment'
const NUMERIC_ASSESSMENT_NODE = 'ObojoboDraft.Chunks.NumericAssessment'
+const MATERIA_ASSESSMENT_NODE = 'ObojoboDraft.Chunks.MateriaAssessment'
const ASSESSMENT_NODE = 'ObojoboDraft.Sections.Assessment'
const PAGE_NODE = 'ObojoboDraft.Pages.Page'
const TEXT_NODE = 'ObojoboDraft.Chunks.Text'
@@ -68,6 +69,7 @@ const Question = props => {
const type = event.target.value
const item = Common.Registry.getItemForType(type)
+
const newBlock = item.cloneBlankNode()
// preserve whether this question is a survey or not
@@ -180,6 +182,24 @@ const Question = props => {
` is-type-${content.type}` +
` is-${content.collapsed ? 'collapsed' : 'not-collapsed'}`
+ const questionTypeOptions = [
+ ,
+
+ ]
+
+ // only add the Materia widget option if the Materia assessment node has been registered
+ if (Common.Registry.contentTypes.includes(MATERIA_ASSESSMENT_NODE)) {
+ questionTypeOptions.push(
+
+ )
+ }
+
return (
{
- {!isAssessmentQuestion ? (
+ {!isAssessmentQuestion && !noFooter ? (
{
expect(tree).toMatchSnapshot()
})
+
+ test('Does not render a QuestionFooter in practice mode if the assessment has the appropriate setting', () => {
+ const props = getDefaultProps({
+ questionType: 'default',
+ mode: 'practice',
+ viewState: 'active',
+ response: null,
+ shouldShowRevealAnswerButton: false,
+ isAnswerRevealed: false,
+ isShowingExplanation: false,
+ isShowingExplanationButton: false,
+ score: null
+ })
+ props.questionAssessmentModel.modelState = { noFooter: true }
+
+ const component = renderer.create()
+
+ expect(component.root.findAllByType(QuestionFooter).length).toBe(0)
+ })
+
+ test('Renders a QuestionFooter in review mode regardles of the noFooter setting', () => {
+ const props = getDefaultProps({
+ questionType: 'default',
+ mode: 'review',
+ viewState: 'active',
+ response: null,
+ shouldShowRevealAnswerButton: false,
+ isAnswerRevealed: false,
+ isShowingExplanation: false,
+ isShowingExplanationButton: false,
+ score: null
+ })
+ props.questionAssessmentModel.modelState = { noFooter: true }
+
+ const component = renderer.create()
+
+ expect(component.root.findAllByType(QuestionFooter).length).toBe(1)
+ })
})
diff --git a/packages/obonode/obojobo-chunks-question/question-outcome.js b/packages/obonode/obojobo-chunks-question/question-outcome.js
index 71e3224a92..449b9497d2 100644
--- a/packages/obonode/obojobo-chunks-question/question-outcome.js
+++ b/packages/obonode/obojobo-chunks-question/question-outcome.js
@@ -4,7 +4,8 @@ const QuestionOutcome = props => {
const isModeSurvey = props.type === 'survey'
const score = props.score
const isForScreenReader = props.isForScreenReader
- const isCorrect = score === 100
+ const isCorrect = score >= 100
+ const isIncorrect = score === 0 || score === 'no-score'
if (isModeSurvey) {
return (
@@ -30,7 +31,7 @@ const QuestionOutcome = props => {
)
}
- if (!isCorrect && isForScreenReader) {
+ if (isIncorrect && isForScreenReader) {
return (
{`${props.feedbackText} - You received a ${score}% on this question.`}
@@ -38,12 +39,65 @@ const QuestionOutcome = props => {
)
}
+ if (isIncorrect && !isForScreenReader) {
+ return (
+
+ )
+ }
+
/*if (!isCorrect && !isForScreenReader) */
+ // return (
+ //
+ //
{props.feedbackText}
+ //
+ // )
return (
-
{props.feedbackText}
+
{`${props.feedbackText} - You received a ${score}% on this question.`}
)
}
export default QuestionOutcome
+
+// import React from 'react'
+
+// const QuestionOutcome = props => {
+// const isModeSurvey = props.type === 'survey'
+// const score = props.score
+// const isForScreenReader = props.isForScreenReader
+
+// let resultClass = ''
+// let resultText = `${props.feedbackText} - You received a ${score}% on this question.`
+
+// switch (true) {
+// case score === 'no-score':
+// resultText = props.feedbackText
+// resultClass = 'correct'
+// break
+// case score === 0:
+// if (isModeSurvey || !isForScreenReader) resultText = props.feedbackText
+// resultClass = 'incorrect'
+// break
+// case score >= 100:
+// if (isModeSurvey || !isForScreenReader) resultText = props.feedbackText
+// resultClass = 'correct'
+// break
+// case score > 0 && score < 100:
+// default:
+// resultClass = 'partially-correct'
+// break
+// }
+// // survey overrides other types
+// if (isModeSurvey) resultClass = 'survey'
+
+// return (
+//
+// )
+// }
+
+// export default QuestionOutcome
diff --git a/packages/obonode/obojobo-chunks-question/viewer-component.js b/packages/obonode/obojobo-chunks-question/viewer-component.js
index 5886620e23..723d349839 100644
--- a/packages/obonode/obojobo-chunks-question/viewer-component.js
+++ b/packages/obonode/obojobo-chunks-question/viewer-component.js
@@ -39,6 +39,7 @@ export default class Question extends React.Component {
this.onClickReveal = this.onClickReveal.bind(this)
this.onFormSubmit = this.onFormSubmit.bind(this)
this.onFormChange = this.onFormChange.bind(this)
+ this.onSaveAnswer = this.onSaveAnswer.bind(this)
this.onClickShowExplanation = this.onClickShowExplanation.bind(this)
this.onClickHideExplanation = this.onClickHideExplanation.bind(this)
this.isShowingExplanation = this.isShowingExplanation.bind(this)
@@ -79,7 +80,9 @@ export default class Question extends React.Component {
return true
}
- onFormChange(event) {
+ // This is passed down the chain directly to assessment chunks
+ // In the event that they are not forms, they can still submit answers
+ onSaveAnswer(event) {
if (this.props.isReview) return
const prevResponse = QuestionUtil.getResponse(
@@ -103,6 +106,10 @@ export default class Question extends React.Component {
this.nextFocus = FOCUS_TARGET_RESULTS
}
+ onFormChange(event) {
+ this.onSaveAnswer(event)
+ }
+
onFormSubmit(event) {
event.preventDefault()
@@ -181,6 +188,7 @@ export default class Question extends React.Component {
const feedbackText = getLabel(
modelState.correctLabels,
+ modelState.partialLabels,
modelState.incorrectLabels,
calculatedScoreResponse.score,
this.props.mode === 'review',
@@ -339,6 +347,7 @@ export default class Question extends React.Component {
return getLabel(
this.props.model.modelState.correctLabels,
+ this.props.model.modelState.partialLabels,
this.props.model.modelState.incorrectLabels,
this.getScore(),
this.getMode() === 'review',
@@ -536,6 +545,7 @@ export default class Question extends React.Component {
onClickShowExplanation={this.onClickShowExplanation}
onClickHideExplanation={this.onClickHideExplanation}
onClickBlocker={this.onClickBlocker}
+ onSaveAnswer={this.onSaveAnswer}
onEnterPress={this.onEnterPress}
/>
)
diff --git a/packages/obonode/obojobo-chunks-question/viewer-component.scss b/packages/obonode/obojobo-chunks-question/viewer-component.scss
index 6c00c4c96e..c7203c9279 100644
--- a/packages/obonode/obojobo-chunks-question/viewer-component.scss
+++ b/packages/obonode/obojobo-chunks-question/viewer-component.scss
@@ -618,6 +618,10 @@
background-color: $color-correct;
}
+ &.partially-correct {
+ background-color: $color-partially-correct;
+ }
+
&.incorrect {
background-color: $color-incorrect;
}
diff --git a/packages/obonode/obojobo-chunks-question/viewer-component.test.js b/packages/obonode/obojobo-chunks-question/viewer-component.test.js
index 8d0410583d..4ac4f76a74 100644
--- a/packages/obonode/obojobo-chunks-question/viewer-component.test.js
+++ b/packages/obonode/obojobo-chunks-question/viewer-component.test.js
@@ -52,6 +52,7 @@ const questionJSON = {
type: 'ObojoboDraft.Chunks.MCAssessment',
content: {
correctLabels: 'mock-correct-labels',
+ partialLabels: 'mock-partial-labels',
incorrectLabels: 'mock-incorrect-labels'
},
children: [
diff --git a/packages/obonode/obojobo-sections-assessment/components/assessment-score-report-view.scss b/packages/obonode/obojobo-sections-assessment/components/assessment-score-report-view.scss
index 0b2a7b0344..a3819eb5a8 100644
--- a/packages/obonode/obojobo-sections-assessment/components/assessment-score-report-view.scss
+++ b/packages/obonode/obojobo-sections-assessment/components/assessment-score-report-view.scss
@@ -3,6 +3,7 @@
.obojobo-draft--sections--assessment--components--score-report {
$color-hr: transparentize(rgba(0, 0, 0, 0.3), 0.2);
$color-correct: #38ae24;
+ $color-partially-correct: #ffc802;
$color-incorrect: #c21f00;
$color-bg: white;
$color-bg2: #f4f4f4;
diff --git a/packages/obonode/obojobo-sections-assessment/components/full-review/__snapshots__/basic-review.test.js.snap b/packages/obonode/obojobo-sections-assessment/components/full-review/__snapshots__/basic-review.test.js.snap
index d6dc4ec098..e928d8a4a7 100644
--- a/packages/obonode/obojobo-sections-assessment/components/full-review/__snapshots__/basic-review.test.js.snap
+++ b/packages/obonode/obojobo-sections-assessment/components/full-review/__snapshots__/basic-review.test.js.snap
@@ -174,6 +174,7 @@ exports[`BasicReview Basic component correct 1`] = `
"collapsed": false,
"correctLabels": null,
"incorrectLabels": null,
+ "partialLabels": null,
"revealAnswer": "default",
"solution": Object {
"children": Array [
@@ -399,6 +400,7 @@ exports[`BasicReview Basic component incorrect 1`] = `
"collapsed": false,
"correctLabels": null,
"incorrectLabels": null,
+ "partialLabels": null,
"revealAnswer": "default",
"solution": Object {
"children": Array [
@@ -624,6 +626,7 @@ exports[`BasicReview Basic component survey 1`] = `
"collapsed": false,
"correctLabels": null,
"incorrectLabels": null,
+ "partialLabels": null,
"revealAnswer": "default",
"solution": Object {
"children": Array [