Skip to content

Commit

Permalink
ucfopen#34 Adjustments to download/install window following changes t…
Browse files Browse the repository at this point in the history
…o the output from the preflight checklist. Covered all checks so far.
  • Loading branch information
FrenjaminBanklin committed Mar 27, 2019
1 parent acdca6e commit 4b617b1
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 14 deletions.
14 changes: 8 additions & 6 deletions assets/css/mdk-download.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ body#download #preflight-checklist{
margin: 0 auto;
}

body#download #preflight-checklist > span .action{
font-size: 10px;
body#download #preflight-checklist > span.action{
display: block;
margin-left: 20px;
font-size: 12px;
}

body#download #preflight-checklist > span:before{
Expand All @@ -54,15 +56,15 @@ body#download #preflight-checklist > span:before{
width: 20px;
}

body#download #preflight-checklist > span.pass:before{
body#download #preflight-checklist > span:not(.action).pass:before{
content: "✔";
color: green;
}
body#download #preflight-checklist > span.fail:before{
body#download #preflight-checklist > span:not(.action).fail:before{
content: "X";
color: red;
}
body#download #preflight-checklist > span.unknown:before{
body#download #preflight-checklist > span:not(.action).unknown:before{
content: "?";
color: orange;
}
}
297 changes: 292 additions & 5 deletions express.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,30 @@ var getQuestion = (ids) => {
return qlist;
};

const INSTALL_TYPE_NUMBER = 'number'
const INSTALL_TYPE_BOOLEAN = 'boolean'
const INSTALL_TYPE_STRING = 'string'
const INSTALL_TYPE_ARRAY = 'object'

const verifyInstallProp = (prop, desiredType) => {
const propType = typeof prop
if(propType === 'undefined' || propType === 'null') return false
if(desiredType === INSTALL_TYPE_BOOLEAN) {
//yaml parser interprets all valid YAML boolean values as strings
if(propType !== 'string') return false
//if we want a boolean, make sure the string we got is one of the accepted YAML boolean strings
const match = prop.match(/^(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF){1}$/)
if(!match) return false
}
if(desiredType === INSTALL_TYPE_STRING && propType !== 'string') return false
if(desiredType === INSTALL_TYPE_NUMBER && propType !== 'number') return false
if(desiredType === INSTALL_TYPE_ARRAY) {
if(propType !== 'object') return false
if(prop.length < 1) return false
}
return true
}

// app is passed a reference to the webpack dev server (Express.js)
module.exports = (app) => {

Expand Down Expand Up @@ -396,9 +420,10 @@ module.exports = (app) => {
playerCallback: '',
scoreScreenCallback: ''
}
//let's see if we can get the demo json file
let allGood = true
//check demo.json
try {
let demo = JSON.parse(getFileFromWebpack('demo.json').toString())
const demo = JSON.parse(getFileFromWebpack('demo.json').toString())

if(!demo.name) {
status.demo = 'fail'
Expand All @@ -412,11 +437,268 @@ module.exports = (app) => {
} else if(!demo.qset.data) {
status.demo = 'fail'
action.demo = "'qset' 'data' property missing"
} else {
status.demo = 'pass'
}
status.demo = 'pass'
} catch(error) {
//TODO: use the error object to determine why there was a failure
// maybe move the failure contextualization from the try to the catch
status.demo = 'fail'
action.demo = 'demo.json missing or can\'t be parsed'
action.demo = "demo.json missing or can't be parsed"
}

//check install.yaml
//scope this so we can use it for other checks later
let install = null
try {
install = yaml.parse(getInstall().toString())
if(!install.general) {
status.install = 'fail'
action.install = "'general' property missing"
} else if(!verifyInstallProp(install.general.name, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'general' 'name' property missing or not a string"
} else if(!verifyInstallProp(install.general.group, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'general' 'group' property missing or not a string"
} else if(!verifyInstallProp(install.general.height, INSTALL_TYPE_NUMBER)) {
status.install = 'fail'
action.install = "'general' 'height' property missing or not a number"
} else if(!verifyInstallProp(install.general.width, INSTALL_TYPE_NUMBER)) {
status.install = 'fail'
action.install = "'general' 'width' property missing or not a number"
} else if(!verifyInstallProp(install.general.in_catalog, INSTALL_TYPE_BOOLEAN)) {
status.install = 'fail'
action.install = "'general' 'in_catalog' property missing or not a boolean"
} else if(!verifyInstallProp(install.general.is_editable, INSTALL_TYPE_BOOLEAN)) {
status.install = 'fail'
action.install = "'general' 'is_editable' property missing or not a boolean"
} else if(!verifyInstallProp(install.general.is_playable, INSTALL_TYPE_BOOLEAN)) {
status.install = 'fail'
action.install = "'general' 'is_playable' property missing or not a boolean"
} else if(!verifyInstallProp(install.general.is_qset_encrypted, INSTALL_TYPE_BOOLEAN)) {
status.install = 'fail'
action.install = "'general' 'is_qset_encrypted' property missing or not a boolean"
} else if(!verifyInstallProp(install.general.api_version, INSTALL_TYPE_NUMBER)) {
status.install = 'fail'
action.install = "'general' 'api_version' property missing or not a number"
} else if(!install.files) {
status.install = 'fail'
action.install = "'files' property missing"
} else if(!verifyInstallProp(install.files.creator, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'files' 'creator' property missing or not a string"
} else if(!verifyInstallProp(install.files.player, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'files' 'player' property missing or not a string"
} else if(!verifyInstallProp(install.files.flash_version, INSTALL_TYPE_NUMBER)) {
status.install = 'fail'
action.install = "'files' 'flash_version' property missing or not a number"
} else if(!install.score) {
status.install = 'fail'
action.install = "'score' property missing"
} else if(!verifyInstallProp(install.score.is_scorable, INSTALL_TYPE_BOOLEAN)) {
status.install = 'fail'
action.install = "'score' 'is_scorable' property missing or not a boolean"
} else if(!verifyInstallProp(install.score.score_module, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'score' 'score_module' property missing or not a string"
} else if(install.score.score_screen && !verifyInstallProp(install.score.score_screen, INSTALL_TYPE_STRING)) {
//custom score screens are optional
status.install = 'fail'
action.install = "'score' 'score_screen' property not a string"
} else if(!install.meta_data) {
status.install = 'fail'
action.install = "'meta_data' property missing"
} else if(!verifyInstallProp(install.meta_data.features, INSTALL_TYPE_ARRAY)) {
status.install = 'fail'
action.install = "'meta_data' 'features' property missing, not an array, or empty"
} else if(!verifyInstallProp(install.meta_data.supported_data, INSTALL_TYPE_ARRAY)) {
status.install = 'fail'
action.install = "'meta_data' 'supported_data' property missing, not an array, or empty"
} else if(!verifyInstallProp(install.meta_data.about, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'meta_data' 'about' property missing or not a string"
} else if(!verifyInstallProp(install.meta_data.excerpt, INSTALL_TYPE_STRING)) {
status.install = 'fail'
action.install = "'meta_data' 'excerpt' property missing or not a string"
} else {
status.install = 'pass'
}
} catch(error) {
status.install = 'fail'
action.install = "install.yaml missing or can't be parsed"
}

status.screenshot = 'pass'
//check screenshots
for(let i = 1; i <= 3; i++) {
try {
getFileFromWebpack(path.join('img','screen-shots',`${i}.png`))
} catch(error) {
status.screenshot = 'fail'
action.screenshot = `file 'src/_screen-shots/${i}.png' missing`
}
try {
getFileFromWebpack(path.join('img','screen-shots',`${i}-thumb.png`))
} catch(error) {
status.screenshot = 'fail'
action.screenshot = `file 'src/_screen-shots/${i}-thumb.png' missing`
}
}

//check icons
const iconSizes = [60,92,275,394]
status.icon = 'pass'
iconSizes.forEach(size => {
try {
getFileFromWebpack(path.join('img',`icon-${size}.png`))
} catch(error) {
status.icon = 'fail'
action.icon = `file 'src/_icons/icon-${size}.png' missing`
}
try {
getFileFromWebpack(path.join('img',`icon-${size}@2x.png`))
} catch(error) {
status.icon = 'fail'
action.icon = `file 'src/_icons/icon-${size}@2x.png' missing`
}
})

//check score module
if(install && install.score.score_module) {
try {
//running regular expressions on a string representation of the score module should be good enough
const scoreModule = getFileFromWebpack(path.join('_score-modules', 'score_module.php')).toString()

const phpOpenMatch = scoreModule.match(/^<\?php$/gm)
const namespaceMatch = scoreModule.match(/^namespace Materia;$/gm)
//get the name of the score module this widget uses from install.yaml
const classCheck = new RegExp(`^class Score_Modules_${install.score.score_module} extends Score_Module$`, 'gm')
const classMatch = scoreModule.match(classCheck)
const functionMatch = scoreModule.match(/^\t{1}public function check_answer\(\$(\w)+\)$/gm)
if(!phpOpenMatch || phpOpenMatch.length > 1) {
status.scoreModule = 'fail'
action.scoreModule = "'<?php' missing or used more than once"
} else if(!namespaceMatch || namespaceMatch.length > 1) {
status.scoreModule = 'fail'
action.scoreModule = "'namespace Materia;' missing or used more than once"
} else if(!classMatch || classMatch.length > 1) {
status.scoreModule = 'fail'
action.scoreModule = `score module class 'Score_Modules_${install.score.score_module}' was not defined or defined more than once`
} else if(!functionMatch || functionMatch > 1) {
status.scoreModule = 'fail'
action.scoreModule = "'check_answer' function was not defined or defined more than once"
} else {
status.scoreModule = 'pass'
}

} catch(error) {
status.scoreModule = 'fail'
action.scoreModule = "score module missing or can't be parsed"
}
} else {
//if we can't get the name of the score module we need, we can't check validity
//this shouldn't ever happen if the whole install.yaml check block passes
action.scoreModule = "can't verify score module name from install.yaml"
}

//check creator callbacks
if(install.files.creator != 'default') {
try {
const creator = getFileFromWebpack('creator.js').toString()
let missingCreatorCalls = []
const neededCreatorCallbacks = [
'initNewWidget',
'initExistingWidget',
'onMediaImportComplete',
'onQuestionImportComplete',
'onSaveClicked',
'onSaveComplete'
]
neededCreatorCallbacks.forEach(callback => {
const callbackCheck = new RegExp(`(function ${callback}){1}|(${callback} = function){1}`, 'g')
const callbackMatch = creator.match(callbackCheck)
if(!callbackMatch || callbackMatch.length > 1) {
status.creatorCallback = 'fail'
action.creatorCallback = `'${callback}' method missing or defined more than once`
missingCreatorCalls.push(callback)
}
})
const neededCreatorCoreCalls = [
'save',
// 'cancelSave',
'start'
]
neededCreatorCoreCalls.forEach(coreCall => {
const coreCallCheck = new RegExp(`Materia.CreatorCore.${coreCall}`, 'g')
const coreCallMatch = creator.match(coreCallCheck)
if(!coreCallMatch || coreCallMatch.length > 1) {
status.creatorCallback = 'fail'
action.creatorCallback = `CreatorCore '${coreCall}' method never called`
missingCreatorCalls.push(coreCall)
}
})
if(missingCreatorCalls.length == 0) status.creatorCallback = 'pass'
} catch(error) {
status.creatorCallback = 'fail'
action.creatorCallback = "creator source code missing or can't be parsed"
}
} else {
status.creatorCallback = 'pass'
action.creatorCallback = 'widget using default creator'
}

//check player callbacks
try {
const player = getFileFromWebpack('player.js').toString()
const playerSaveMatch = player.match(/Materia.Engine.start/g)
if(!playerSaveMatch || playerSaveMatch > 1) {
status.playerCallback = 'fail'
action.playerCallback = "EngineCore 'start' method missing or called more than once"
} else {
status.playerCallback = 'pass'
}
} catch(error) {
status.playerCallback = 'fail'
action.playerCallback = "player source code missing or can't be parsed"
}

//check score screen callbacks
if(install.score.score_screen) {
const scoreScreen = getFileFromWebpack('scorescreen.js').toString()
let missingScoreScreenCalls = []
const neededScoreScreenCallbacks = [
'start',
'update'
]
neededScoreScreenCallbacks.forEach(callback => {
const callbackCheck = new RegExp(`(function ${callback}){1}|(${callback} = function){1}`, 'g')
const callbackMatch = scoreScreen.match(callbackCheck)
if(!callbackMatch || callbackMatch.length > 1) {
status.scoreScreenCallback = 'fail'
action.scoreScreenCallback = `'${callback}' method missing or defined more than once`
missingScoreScreenCalls.push(callback)
}
})
const neededScoreCoreCalls = [
// 'hideScoresOverview',
// 'hideResultsTable',
'start'
]
neededScoreCoreCalls.forEach(coreCall => {
const coreCallCheck = new RegExp(`Materia.ScoreCore.${coreCall}`, 'g')
const coreCallMatch = scoreScreen.match(coreCallCheck)
if(!coreCallMatch || coreCallMatch.length > 1) {
status.scoreScreenCallback = 'fail'
action.scoreScreenCallback = `ScoreCore '${coreCall}' method never called`
missingScoreScreenCalls.push(coreCall)
}
})
if(missingScoreScreenCalls.length == 0) status.scoreScreenCallback = 'pass'
} else {
status.scoreScreenCallback = 'pass'
action.scoreScreenCallback = 'widget not using custom score screen'
}

const checklist = [
Expand Down Expand Up @@ -462,7 +744,12 @@ module.exports = (app) => {
},
]

res.locals = Object.assign(res.locals, {template: 'download', checklist: checklist})
//do one more pass over the whole checklist - if there are any failures, prevent build/install
checklist.forEach(item => {
if(item.status == 'fail') allGood = false
})

res.locals = Object.assign(res.locals, {template: 'download', checklist: checklist, allGood: allGood})
res.render(res.locals.template)
})

Expand Down
8 changes: 5 additions & 3 deletions views/download.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ <h2 class='centered'>Preflight Checks:</h2>
<span>
{{text}}
</span>
<span class='action'>
{{action}}
</span>
</span>
<span class='action'>
{{action}}
</span>
{{/checklist}}
</div>
</div>

<hr/>
{{#allGood}}
<div id='build-commands' class='centered'>
<a href="/mdk/download"><button class='edit_button orange' id='download_button'>Download .wigt</button></a>
<a href="/mdk/install"><button class='edit_button orange' id='install_button'>Install to Docker Materia</button></a>
</div>
{{/allGood}}

<div class='centered'>
<a id="cancel-button" href="#">Cancel</a>
Expand Down

0 comments on commit 4b617b1

Please sign in to comment.