From 4432a10a15b2bd9747b1edc1baefe5c31ca89406 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 24 Mar 2026 16:51:43 +0000 Subject: [PATCH 1/4] common code/styles for assessments --- .gitignore | 1 + assessment.css | 53 +++++++++++++++++++ helper.js | 137 +++++++++++++++++++++++++++++++++++++++++++++++++ settings.css | 4 ++ 4 files changed, 195 insertions(+) create mode 100644 .gitignore create mode 100644 assessment.css create mode 100644 helper.js create mode 100644 settings.css 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..d9a8fcf --- /dev/null +++ b/assessment.css @@ -0,0 +1,53 @@ +.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..48757d6 --- /dev/null +++ b/helper.js @@ -0,0 +1,137 @@ +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', + CHECK: 'assessments.check', + RESET: 'assessments.reset', + MODIFY: 'assessments.modify', +} + +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 : '') +} diff --git a/settings.css b/settings.css new file mode 100644 index 0000000..1fb44b5 --- /dev/null +++ b/settings.css @@ -0,0 +1,4 @@ +.settings-row { + display: flex; + flex-direction: column; +} From 3cf81d5d8d2dea1aa5775e7af77cb89ec9f724a6 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Wed, 25 Mar 2026 15:20:40 +0000 Subject: [PATCH 2/4] added isCanAnswerAgain to helper --- helper.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helper.js b/helper.js index 48757d6..78f68a4 100644 --- a/helper.js +++ b/helper.js @@ -14,9 +14,10 @@ window.codioAssessmentsHelper.METHODS = { GET_CONTENT: 'assessments.getContent', SET_CONTENT: 'assessments.setContent', CALLBACK: 'assessments.callback', - CHECK: 'assessments.check', + SUBMIT_ANSWER: 'assessments.submitAnswer', RESET: 'assessments.reset', MODIFY: 'assessments.modify', + UNBLOCK: 'assessments.unblock', } window.codioAssessmentsHelper.callbacks = {} @@ -135,3 +136,8 @@ window.codioAssessmentsHelper.calculateGuidance = ( return showAsTeacher ? guidance : (showGuidance ? answerGuidance : '') } + +window.codioAssessmentsHelper.isCanAnswerAgain = (assessment, result) => { + const usedAttempts = result?.usedAttempts + return !assessment.source.maxAttemptsCount || usedAttempts < assessment.source.maxAttemptsCount +} From 73b2d08dcb7c735828817c53f462eed99091fb25 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Fri, 27 Mar 2026 15:15:13 +0000 Subject: [PATCH 3/4] update styles --- settings.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/settings.css b/settings.css index 1fb44b5..db9e00b 100644 --- a/settings.css +++ b/settings.css @@ -1,3 +1,9 @@ +.settings-content { + display: flex; + gap: 10px; + flex-direction: column; +} + .settings-row { display: flex; flex-direction: column; From 588b22efe4334c8ec9e58a4e0bf9f839b7b65eb8 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Mon, 30 Mar 2026 16:21:13 +0100 Subject: [PATCH 4/4] wip --- assessment.css | 8 ++++++++ helper.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/assessment.css b/assessment.css index d9a8fcf..f3dbc8a 100644 --- a/assessment.css +++ b/assessment.css @@ -1,3 +1,11 @@ +body { + margin: 0 !important; +} + +.hide { + display: none !important; +} + .codio-assessment { padding: 12px 40px 12px 65px; margin: 5px; diff --git a/helper.js b/helper.js index 78f68a4..afa431c 100644 --- a/helper.js +++ b/helper.js @@ -20,6 +20,20 @@ window.codioAssessmentsHelper.METHODS = { 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 = () => { @@ -141,3 +155,20 @@ 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 + ) +}