diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09c56d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea diff --git a/assessment.css b/assessment.css new file mode 100644 index 0000000..f3dbc8a --- /dev/null +++ b/assessment.css @@ -0,0 +1,61 @@ +body { + margin: 0 !important; +} + +.hide { + display: none !important; +} + +.codio-assessment { + padding: 12px 40px 12px 65px; + margin: 5px; + font-size: 0.9em; + color: #484040; + background-color: #f5f9f5; + border-radius: 4px; + box-shadow: 2px 2px 4px #DDD; + font-family: 'Nunito Sans', sans-serif; +} + +.codio-assessment-name { + font-size: 1.2em; + font-style: normal; + margin-bottom: 0; + margin-top: 0; + border: none; + padding-bottom: 0; +} + +.codio-assessment-button { + display: inline-block; + padding: 3px 17px 1px; + font-size: 75%; + cursor: pointer; + background: #386fd7; + border-radius: 2px; + color: #fff; + border: 1px solid #386fd7; +} + +.codio-assessment-button:disabled { + cursor: default; + opacity: 0.8; +} + +.codio-assessment-guidance-container { + padding: 0.5em; + margin-bottom: 1em; + background-color: #e4efe4; + border: 1px #d6ded6 solid; + border-radius: 3px; +} + +.codio-assessment-guidance-text { + font-size: 0.9em; +} + +.codio-assessment-footer { + display: flex; + flex-direction: row; + gap: 10px; +} diff --git a/helper.js b/helper.js new file mode 100644 index 0000000..afa431c --- /dev/null +++ b/helper.js @@ -0,0 +1,174 @@ +window.codioAssessmentsHelper = window.codioAssessmentsHelper || {} + +window.codioAssessmentsHelper.METHODS = { + GET_SETTINGS: 'assessments.getSettings', + GET_SETTINGS_RESPONSE: 'assessments.getSettings.response', + EXPORT_SETTINGS: 'assessments.exportSettings', + EXPORT_SETTINGS_RESPONSE: 'assessments.exportSettings.response', + GET_STYLES: 'assessments.getStyles', + GET_STYLES_RESPONSE: 'assessments.getStyles.response', + GET_STATE: 'assessments.getState', + GET_STATE_RESPONSE: 'assessments.getState.response', + SET_STATE: 'assessments.setState', + SET_HEIGHT: 'assessments.setHeight', + GET_CONTENT: 'assessments.getContent', + SET_CONTENT: 'assessments.setContent', + CALLBACK: 'assessments.callback', + SUBMIT_ANSWER: 'assessments.submitAnswer', + RESET: 'assessments.reset', + MODIFY: 'assessments.modify', + UNBLOCK: 'assessments.unblock', +} + +window.codioAssessmentsHelper.States = { + FAIL: 'fail', + PASS: 'pass', + RESET: 'reset', + PROGRESS: 'progress', + PENDING: 'pending' +} + +window.codioAssessmentsHelper.PreviewType = { + NONE: 'NONE', + MARKDOWN: 'MARKDOWN', + RAW: 'RAW' +} + +window.codioAssessmentsHelper.callbacks = {} + +window.codioAssessmentsHelper.deferred = () => { + let resolve, reject + const promise = new Promise((resolveF, rejectF) => { + resolve = resolveF + reject = rejectF + }) + return { resolve, reject, promise } +} + +window.codioAssessmentsHelper.send = (methodName, data) => { + const id = window.location.hash.substring(1) + console.log('assessment iframe send', methodName, data) + window.parent.postMessage(JSON.stringify({id, method: methodName, data}), '*') +} + +window.codioAssessmentsHelper.sendAndWait = (methodName, data = {}) => { + const id = `id_${Date.now()}` + const dfd = window.codioAssessmentsHelper.deferred() + window.codioAssessmentsHelper.callbacks[id] = (data) => data && data.error ? dfd.reject(new Error(data.error)) : dfd.resolve(data) + data.callbackId = id + window.codioAssessmentsHelper.send(methodName, data) + return dfd.promise +} + +window.codioAssessmentsHelper.processCallback = (data) => { + if (!data) { + return + } + const {callbackId, ...result} = data + window.codioAssessmentsHelper.callbacks[callbackId] && window.codioAssessmentsHelper.callbacks[callbackId](result) +} + +window.codioAssessmentsHelper.registerMessageListener = listener => { + window.addEventListener( + 'message', + (event) => { + listener(event.data) + }, + false + ) +} + +window.codioAssessmentsHelper.getBodyHeight = () => { + const body = document.body + const html = document.documentElement + return Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) +} + +window.codioAssessmentsHelper.addBodyHeightListener = () => { + const debounceSetHeight = window.codioAssessmentsHelper.debounce(() => { + window.codioAssessmentsHelper.send( + window.codioAssessmentsHelper.METHODS.SET_HEIGHT, {height: window.codioAssessmentsHelper.getBodyHeight()}) + }, 100) + const resizeObserver = new ResizeObserver(debounceSetHeight) + resizeObserver.observe(document.body) +} + +window.codioAssessmentsHelper.addStyle = (() => { + const style = document.createElement('style') + document.head.append(style) + return (styleString) => style.textContent = styleString +})() + +window.codioAssessmentsHelper.debounce = (func, timeout) => { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { func.apply(this, args); }, timeout); + }; +} + +window.codioAssessmentsHelper.getButtonCaption = (assessmentOptions, maxAttemptsCount) => { + const {usedAttempts, buttonCaption} = assessmentOptions + let caption = buttonCaption + if (maxAttemptsCount) { + const attemptsLeftCount = usedAttempts < maxAttemptsCount ? maxAttemptsCount - usedAttempts : 0 + const attemptsLeft = attemptsLeftCount ? ` (${attemptsLeftCount} left)` : '' + caption = `${caption}${attemptsLeft}` + } + return caption +} + +window.codioAssessmentsHelper.calculateGuidance = ( + authoringMode, + showAsTeacher, + answered, + {showGuidanceAfterResponseOption, guidance, points}, + {answerGuidance, answerPoints, attemptsCount, passed, isCompletedAndReleased} +) => { + if (authoringMode) { + let showGuidanceAfterResponse = false + if (!showGuidanceAfterResponseOption) { + showGuidanceAfterResponse = false + } else if (showGuidanceAfterResponseOption.type === 'Always') { + showGuidanceAfterResponse = true + } else if (showGuidanceAfterResponseOption.type === 'Attempts') { + showGuidanceAfterResponse = attemptsCount >= showGuidanceAfterResponseOption.passedFrom || passed + } else if (showGuidanceAfterResponseOption.type === 'Score' && answered) { + showGuidanceAfterResponse = points <= 0 || + (answerPoints * 100 / points) >= showGuidanceAfterResponseOption.passedFrom + } else if (showGuidanceAfterResponseOption.type === 'WhenGradesReleased') { + showGuidanceAfterResponse = true + } + return showAsTeacher || answered && showGuidanceAfterResponse ? guidance : '' + } + + // for student, it is calculated on server side + let showGuidance = answered + if (showGuidanceAfterResponseOption?.type === 'WhenGradesReleased') { + showGuidance = answered && isCompletedAndReleased + } + + return showAsTeacher ? guidance : (showGuidance ? answerGuidance : '') +} + +window.codioAssessmentsHelper.isCanAnswerAgain = (assessment, result) => { + const usedAttempts = result?.usedAttempts + return !assessment.source.maxAttemptsCount || usedAttempts < assessment.source.maxAttemptsCount +} + +const getAssignmentSettings = (assignment) => { + return assignment.projectBased?.settings || assignment.bookBased?.settings +} + +window.codioAssessmentsHelper.calculateCompletedAndReleased = (eduStartedAssignmentInfo) => { + if (!eduStartedAssignmentInfo) { + return false + } + + const { assignment, started } = eduStartedAssignmentInfo + + return ( + started?.completed?.completedAt && + getAssignmentSettings(assignment).releaseGrades + ) +} diff --git a/settings.css b/settings.css new file mode 100644 index 0000000..db9e00b --- /dev/null +++ b/settings.css @@ -0,0 +1,10 @@ +.settings-content { + display: flex; + gap: 10px; + flex-direction: column; +} + +.settings-row { + display: flex; + flex-direction: column; +}