Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 10 additions & 4 deletions packages/obonode/obojobo-chunks-abstract-assessment/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const CHOICE_NODE = 'ObojoboDraft.Chunks.AbstractAssessment.Choice'
const FEEDBACK_NODE = 'ObojoboDraft.Chunks.AbstractAssessment.Feedback'
const MATERIA_NODE = 'ObojoboDraft.Chunks.Materia'

// Whenever an inheriting component is created
// Add its Assessment type to the valid assessments, its answer type to valid answers
Expand All @@ -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/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_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_NODE]: materiaAssessment.children[0]
}

const answerToAssessment = {
[NUMERIC_ANSWER_NODE]: numericAssessment,
[MC_ANSWER_NODE]: mcAssessment
[MC_ANSWER_NODE]: mcAssessment,
[MATERIA_NODE]: materiaAssessment
}

const validAssessments = [NUMERIC_ASSESSMENT_NODE, MC_ASSESSMENT_NODE]
const validAssessments = [NUMERIC_ASSESSMENT_NODE, MC_ASSESSMENT_NODE, MATERIA_NODE]
const validAnswers = [NUMERIC_ANSWER_NODE, MC_ANSWER_NODE]

export {
Expand Down
3 changes: 2 additions & 1 deletion packages/obonode/obojobo-chunks-materia/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
clientScripts: {
viewer: 'viewer.js',
editor: 'editor.js'
}
},
serverScripts: ['server/materiaassessment']
}
}
27 changes: 27 additions & 0 deletions packages/obonode/obojobo-chunks-materia/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const router = require('express').Router() //eslint-disable-line new-cap
const logger = require('obojobo-express/server/logger')
const uuid = require('uuid').v4
const bodyParser = require('body-parser')
const db = require('obojobo-express/server/db')
const oboEvents = require('obojobo-express/server/obo_events')
const Visit = require('obojobo-express/server/models/visit')
const Draft = require('obojobo-express/server/models/draft')
Expand Down Expand Up @@ -210,4 +211,30 @@ router
renderLtiLaunch(launchParams, method, endpoint, res)
})

router
.route('/materia-lti-score-verify')
.get([requireCurrentUser, requireCurrentVisit])
.get(async (req, res) => {
await db
.one(
`SELECT payload
FROM events
WHERE action = 'materia:ltiScorePassback'
AND visit_id = $[visitId]
AND payload->>'lisResultSourcedId' = $[resourceId]
ORDER BY created_at DESC
LIMIT 1`,
{
visitId: req.currentVisit.id,
resourceId: `${req.currentVisit.id}__${req.query.nodeId}`
}
)
.then(result => {
res.send({
score: result.payload.score,
success: result.payload.success
})
})
})

module.exports = router
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const DraftNode = require('obojobo-express/server/models/draft_node')

class NumericAssessment extends DraftNode {
static get nodeName() {
return 'ObojoboDraft.Chunks.Materia'
}

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 = NumericAssessment
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
114 changes: 100 additions & 14 deletions packages/obonode/obojobo-chunks-materia/viewer-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import IFrame from 'obojobo-chunks-iframe/viewer-component'
import React from 'react'
import isOrNot from 'obojobo-document-engine/src/scripts/common/util/isornot'
import TextGroupEl from 'obojobo-document-engine/src/scripts/common/chunk/text-chunk/text-group-el'
import API from 'obojobo-document-engine/src/scripts/viewer/util/api'
import QuestionUtil from 'obojobo-document-engine/src/scripts/viewer/util/question-util'

import Viewer from 'obojobo-document-engine/src/scripts/viewer'
const { NavUtil } = Viewer.util

import './viewer-component.scss'
import IFrameControlTypes from 'obojobo-chunks-iframe/iframe-control-types'
Expand All @@ -10,23 +15,32 @@ 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,
// autoload: props.mode === 'review', // could be annoying either way, maybe better overall if not automatic
controls: [IFrameControlTypes.RELOAD],
sizing: IFrameSizingTypes.FIXED
}

// state setup
this.state = {
model,
visitId: props.moduleData.navState.visitId,
nodeId: props.model.id,
score: null,
verifiedScore: false,
open: false
Expand All @@ -37,10 +51,17 @@ export default class Materia extends React.Component {
this.onShow = this.onShow.bind(this)
}

static isResponseEmpty(response) {
return !response.verifiedScore
}

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) {
// 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
}

Expand All @@ -58,7 +79,38 @@ export default class Materia extends React.Component {

switch (data.type) {
case 'materiaScoreRecorded':
this.setState({ score: data.score })
// this should probably be abstracted in a util function somewhere
API.get(
`/materia-lti-score-verify?visitId=${this.state.visitId}&nodeId=${this.state.nodeId}`,
'json'
)
.then(API.processJsonResults)
.then(result => {
const newState = {
score: result.score,
verifiedScore: true
}
this.setState({
...this.state,
...newState
})

const modelId = this.props.questionModel.get('id')
const moduleContext = NavUtil.getContext(this.props.moduleData.navState)

QuestionUtil.setResponse(
modelId,
{
...newState,
scoreUrl: data.score_url
},
null,
moduleContext,
moduleContext.split(':')[1],
moduleContext.split(':')[2],
false
)
})
break
}
} catch (e) {
Expand All @@ -83,15 +135,29 @@ export default class Materia extends React.Component {
}

renderTextCaption() {
return this.state.model.modelState.textGroup.first.text ? (
<div className="label">
<TextGroupEl
parentModel={this.state.model}
textItem={this.state.model.modelState.textGroup.first}
groupIndex="0"
/>
</div>
) : null
let textCaptionRender = null

let scoreRender = null
if (this.state.score && this.state.verifiedScore) {
scoreRender = (
<span className={'materia-score verified'}>Your highest score: {this.state.score}%</span>
)
}

if (this.state.model.modelState.textGroup.first.text) {
textCaptionRender = (
<div className="label">
<TextGroupEl
parentModel={this.state.model}
textItem={this.state.model.modelState.textGroup.first}
groupIndex="0"
/>
{scoreRender}
</div>
)
}

return textCaptionRender
}

renderCaptionOrScore() {
Expand All @@ -103,6 +169,26 @@ export default class Materia extends React.Component {
}
}

getInstructions() {
return (
<React.Fragment>
<span className="for-screen-reader-only">Embedded Materia widget.</span>
Play the embedded Materia widget to receive a score. Your highest score will be saved.
</React.Fragment>
)
}

calculateScore() {
if (!this.props.score) {
return null
}

return {
score: this.props.score,
details: null
}
}

render() {
return (
<div className={`obojobo-draft--chunks--materia ${isOrNot(this.state.open, 'open')}`}>
Expand Down
11 changes: 7 additions & 4 deletions packages/obonode/obojobo-chunks-materia/viewer-component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading