diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d963de7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/.idea
+/*.iml
diff --git a/assets/images/checkmark.svg b/assets/images/checkmark.svg
new file mode 100644
index 0000000..213cb74
--- /dev/null
+++ b/assets/images/checkmark.svg
@@ -0,0 +1,8 @@
+
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..179647f
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,35 @@
+
+
+
diff --git a/index.css b/index.css
new file mode 100644
index 0000000..92be186
--- /dev/null
+++ b/index.css
@@ -0,0 +1,104 @@
+.codio-assessment-answers {
+ padding: 1em 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+}
+
+.codio-assessment-mcq-answer-input-label {
+ display: flex;
+ gap: 10px;
+}
+
+.codio-assessment-mcq-answer-input-indicator {
+ transition: 0.3s;
+}
+
+.codio-assessment-mcq-answer-input:not([disabled]) .codio-assessment-mcq-answer-input-label {
+ cursor: pointer;
+}
+
+.codio-assessment-mcq-answer-checkbox {
+ .codio-assessment-mcq-answer-input-indicator {
+ background-color: #aaadbd;
+ border-radius: 2px;
+ height: 22px;
+ width: 22px;
+ position: relative;
+ }
+
+ .codio-assessment-mcq-answer-input:not([disabled]) .codio-assessment-mcq-answer-input-label {
+ &:hover {
+ .codio-assessment-mcq-answer-input-indicator {
+ background-color: #797d92;
+ }
+ }
+ }
+
+ .codio-assessment-mcq-answer-input:checked + .codio-assessment-mcq-answer-input-label .codio-assessment-mcq-answer-input-indicator {
+ background-image: url(./assets/images/checkmark.svg);
+ background-size: 60%;
+ background-color: #797d92;
+ background-position: center;
+ background-repeat: no-repeat;
+ }
+}
+
+.codio-assessment-mcq-answer-radio {
+ .codio-assessment-mcq-answer-input-indicator {
+ box-sizing: border-box;
+ border-radius: 50%;
+ height: 22px;
+ width: 22px;
+ position: relative;
+ border: 2px solid #aaadbd;
+ background-color: transparent;
+ }
+
+ .codio-assessment-mcq-answer-input:not([disabled]) .codio-assessment-mcq-answer-input-label {
+ &:hover {
+ .codio-assessment-mcq-answer-input-indicator {
+ border-color: #797d92;
+ }
+ }
+ }
+
+ .codio-assessment-mcq-answer-input:checked + .codio-assessment-mcq-answer-input-label .codio-assessment-mcq-answer-input-indicator {
+ border-color: #797d92;
+ &::before {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ bottom: 2px;
+ right: 2px;
+ background-color: #797d92;
+ border-radius: 50%;
+ }
+ }
+}
+
+.codio-assessment-mcq-answer-icon {
+ position: absolute;
+ width: 22px;
+ height: 22px;
+ left: -36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 100%;
+ color: white;
+}
+
+.codio-assessment-mcq-answer-icon svg {
+ width: 16px;
+ height: 16px;
+}
+
+.codio-assessment-mcq-answer-icon-ok {
+ background-color: #71b380;
+}
+
+.codio-assessment-mcq-answer-icon-wrong {
+ background-color: #da8181;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..adedd40
--- /dev/null
+++ b/index.html
@@ -0,0 +1,30 @@
+
+
+
+ ').html(guidance)
+ guidanceContainer.append(guidanceText)
+ guidanceBlock.append(guidanceContainer)
+ }
+ }
+
+ const isAnswerFullyCorrect = (result) => {
+ if (!result?.right || result.right.length === 0) {
+ return false
+ }
+ if (assessment.source.points === 0) {
+ return result.right.sort().join('_') === result.current.sort().join('_')
+ }
+ return result.state === window.codioAssessmentsHelper.States.PASS && assessment.source.points === result.points
+ }
+
+ const isSomethingChecked = () => {
+ return !!$('.codio-assessment-mcq-answer input:checked').length
+ }
+
+ const getAssessmentState = () => {
+ const result = currentData ? currentData.result : null
+ const answered = result?.state && result.state !== window.codioAssessmentsHelper.States.RESET
+ const answerFullyCorrect = answered && isAnswerFullyCorrect(result, assessment.source)
+ const usedAttempts = result?.usedAttempts
+ const canAnswerAgain = !assessment.source.maxAttemptsCount || usedAttempts < assessment.source.maxAttemptsCount
+ const isDisabled = assessmentOptions.isDisabled || processing || !isSomethingChecked() ||
+ answered && (!canAnswerAgain || answerFullyCorrect)
+ const showModify = assessmentOptions.showUnblock && !answered
+ const teacherInStudentsProject = assessmentOptions.showAsTeacher && !assessmentOptions.owner
+
+ return {
+ isDisabled,
+ answered,
+ usedAttempts,
+ canAnswerAgain,
+ showModify,
+ teacherInStudentsProject,
+ answerFullyCorrect
+ }
+ }
+
+ const updateAnswersAndFooter = () => {
+ if (!assessment) {
+ return
+ }
+ // processing, new state/results
+ updateAnswers()
+ updateFooterButtons()
+ }
+
+ const onInputChange = () => {
+ updateFooterButtons()
+ window.codioAssessmentsHelper.send(
+ window.codioAssessmentsHelper.METHODS.SET_STATE, {state: {active: getValue()}}
+ )
+ }
+
+ const bindEvents = () => {
+ $('.check-button').on('click', onCheck)
+ $('.unblock-button').on('click', onUnblock)
+ $('.reset-button').on('click', onReset)
+ $('.codio-assessment-mcq-answer input').on('change', onInputChange)
+
+ window.codioAssessmentsHelper.addBodyHeightListener()
+ }
+
+ const render = () => {
+ const container = $('.codio-assessment')
+ const nameEl = container.find('.codio-assessment-name')
+ assessment.source.showName ? nameEl.text(assessment.source.name) : nameEl.remove()
+ renderContent()
+ updateCheckButtonText()
+ renderGuidance()
+ renderAnswers()
+ updateAnswers(getInitialValue())
+ updateFooterButtons()
+ bindEvents()
+ container.removeClass('hide')
+ }
+
+ const processMessage = (jsonData) => {
+ try {
+ const {method, data} = JSON.parse(jsonData)
+ console.log('assessment iframe processMessage', jsonData, method, data)
+ switch (method) {
+ case window.codioAssessmentsHelper.METHODS.GET_STYLES_RESPONSE:
+ window.codioAssessmentsHelper.addStyle(data.css)
+ break
+ case window.codioAssessmentsHelper.METHODS.GET_STATE_RESPONSE:
+ processing = false
+ applyState(data)
+ break
+ case window.codioAssessmentsHelper.METHODS.CALLBACK: {
+ window.codioAssessmentsHelper.processCallback(data)
+ break
+ }
+ }
+ } catch (e) { console.error(e) }
+ }
+
+ window.addEventListener('load', () => {
+ window.codioAssessmentsHelper.registerMessageListener(processMessage)
+ window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_STATE)
+ window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_STYLES)
+ })
+})()
diff --git a/metadata.json b/metadata.json
new file mode 100644
index 0000000..8c9c85b
--- /dev/null
+++ b/metadata.json
@@ -0,0 +1,18 @@
+{
+ "name": "Standard Code Test",
+ "type": "assessment",
+ "properties": {
+ "type": "multiple-choice",
+ "icon": "./icon.svg",
+ "defaultHeight": 500,
+ "gradingControls": {
+ "points": true,
+ "incorrectPoints": true,
+ "allowPartialPoints": true,
+ "useMaximumScore": true,
+ "showExpectedAnswer": true,
+ "definedNumberOfAttempts": true,
+ "rationale": true
+ }
+ }
+}
diff --git a/settings.css b/settings.css
new file mode 100644
index 0000000..b5202d3
--- /dev/null
+++ b/settings.css
@@ -0,0 +1,69 @@
+.answers-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+#add-answer {
+ align-self: start;
+}
+
+.answer-list {
+ display: flex;
+ gap: 10px;
+ flex-direction: column;
+}
+
+.answer-item {
+ display: flex;
+ border: 1px solid rgba(0, 0, 0, 0.6);
+ padding: 10px;
+ gap: 10px;
+ border-radius: 6px;
+
+ .codio-assessment-settings-form-label {
+ flex: 0 0 30px;
+ }
+
+ .answer-item-correct-input-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ }
+}
+
+.answer-item-correct-container, .answer-item-answer-container {
+ display: flex;
+ flex-direction: column;
+}
+
+.answer-item-answer-container {
+ flex: 1;
+}
+
+.answer-item-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 0 20px;
+ justify-content: space-around;
+
+ .action-delete {
+ color: red;
+ }
+}
+
+.answer-item-action-button {
+ background: none;
+ border: none;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+ opacity: 0.8;
+
+ &:hover {
+ opacity: 1;
+ }
+}
diff --git a/settings.html b/settings.html
new file mode 100644
index 0000000..295ec4b
--- /dev/null
+++ b/settings.html
@@ -0,0 +1,44 @@
+
+
+
+
+
Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.js b/settings.js
new file mode 100644
index 0000000..1b4bcb7
--- /dev/null
+++ b/settings.js
@@ -0,0 +1,191 @@
+(function () {
+ const ICONS = {
+ UP: '
',
+ DOWN: '
',
+ DELETE: '
'
+ }
+
+ const ACTION_CLASS = {
+ UP: 'action-up',
+ DOWN: 'action-down',
+ DELETE: 'action-delete',
+ }
+
+ let multipleResponse = false
+
+ const getId = () => window.codioAssessmentsHelper.GUID()
+
+ const collectSettings = () => {
+ const instructions = $('#instructions').val()
+ const multipleResponse = $('#multiple-response').is(':checked')
+ const shuffleAnswers = $('#shuffle-answers').is(':checked')
+ const answers = []
+ $('.answer-item').each(function (index, item) {
+ const $item = $(item)
+ const answerId = $item.data('answer-id') || getId()
+ const answer = $item.find('.answer-item-answer-ta').val()
+ const correct = $item.find('[name="correct"]').is(':checked')
+ answers.push({_id: answerId, answer, correct})
+ })
+
+ return {instructions, multipleResponse, shuffleAnswers, answers};
+ }
+
+ const exportSettings = () => {
+ const data = collectSettings();
+ window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS_RESPONSE, data);
+ }
+
+ const applySettings = (settings = {}) => {
+ multipleResponse = !!settings.multipleResponse
+ $('#instructions').val(settings.instructions || '');
+ $('#multiple-response').prop('checked', settings.multipleResponse);
+ $('#shuffle-answers').prop('checked', settings.shuffleAnswers);
+ const answers = settings.answers || []
+ answers.forEach((item) => {
+ addAnswer(item);
+ })
+ }
+
+ const processMessage = (jsonData) => {
+ console.log('settings iframe processMessage', jsonData)
+ try {
+ const {method, data} = JSON.parse(jsonData);
+ switch (method) {
+ case window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS:
+ exportSettings();
+ break;
+ case window.codioAssessmentsHelper.METHODS.GET_SETTINGS_RESPONSE:
+ applySettings(data.settings);
+ break;
+ }
+ } catch {}
+ }
+
+ const renderToggle = (id, name, value) => {
+ const labelEl = $('
');
+ const inputEl = $(`
`)
+ const sliderEl = $('
')
+ labelEl.append(inputEl)
+ labelEl.append(sliderEl)
+ return labelEl
+ }
+
+ const renderRadio = (id, name, value) => {
+ const labelEl = $('
');
+ const inputEl = $(`
`)
+ return labelEl.append(inputEl)
+ }
+
+ const renderCorrectControl = (id, value) => {
+ const name = 'correct'
+ const inputContainer = $('
')
+ const input = multipleResponse ? renderToggle(id, name, value) : renderRadio(id, name, value)
+ return inputContainer.append(input)
+ }
+
+ const refreshCorrectControls = () => {
+ const items = $('.answer-item')
+ const firstCorrect = items.find('input[name="correct"]:checked').closest('.answer-item').first()
+ const firstCorrectIndex = items.index(firstCorrect)
+ items.each((index, element) => {
+ const $el = $(element)
+ const correctContainer = $el.find('.answer-item-correct-container')
+ $el.find('.answer-item-correct-input-container').remove()
+ correctContainer.append(renderCorrectControl(getId(), index === firstCorrectIndex))
+ })
+ }
+
+ const renderIconButton = (className, icon, title) => {
+ return $(`
`)
+ }
+
+ const addAnswer = (data) => {
+ const listContainer = $('.answer-list')
+ const answerItem = $('
')
+ const itemActionsContainer = $('
')
+ itemActionsContainer.append(renderIconButton(ACTION_CLASS.UP, ICONS.UP, 'Move up'))
+ itemActionsContainer.append(renderIconButton(ACTION_CLASS.DELETE, ICONS.DELETE, 'Delete'))
+ itemActionsContainer.append(renderIconButton(ACTION_CLASS.DOWN, ICONS.DOWN, 'Move down'))
+ const correctContainer = $('
')
+ const id = getId()
+ correctContainer.append(`
`)
+ correctContainer.append(renderCorrectControl(id, false))
+ const answerContainer = $('
')
+ const taId = getId()
+ answerContainer.append(`
`)
+ const answerTa = $(`
`)
+ answerContainer.append(answerTa)
+ answerItem.append(itemActionsContainer)
+ answerItem.append(correctContainer)
+ answerItem.append(answerContainer)
+
+ if (data) {
+ data?._id && answerItem.data('answer-id', data._id)
+ data.correct && answerItem.find('[name="correct"]').prop('checked', true)
+ data.answer && answerItem.find('.answer-item-answer-ta').val(data.answer)
+ }
+ listContainer.append(answerItem)
+ }
+
+ const updateActionButtonStates = () => {
+ const items = $('.answer-item')
+ items.find(`.${ACTION_CLASS.UP}`).prop('disabled', false)
+ items.find(`.${ACTION_CLASS.DOWN}`).prop('disabled', false)
+ items.first().find(`.${ACTION_CLASS.UP}`).prop('disabled', true)
+ items.last().find(`.${ACTION_CLASS.DOWN}`).prop('disabled', true)
+ }
+
+ const onActionUp = (e) => {
+ const element = $(e.currentTarget).closest('.answer-item')
+ const prev = element.prev()
+
+ if (!prev?.length) {
+ return
+ }
+ element.insertBefore(prev);
+ updateActionButtonStates()
+ }
+
+ const onActionDown = (e) => {
+ const element = $(e.currentTarget).closest('.answer-item')
+ const next = element.next()
+ if (!next?.length) {
+ return
+ }
+ element.insertAfter(next);
+ updateActionButtonStates()
+ }
+
+ const onActionDelete = (e) => {
+ const element = $(e.currentTarget).closest('.answer-item')
+ element.remove()
+ updateActionButtonStates()
+ }
+
+ const bindEvents = () => {
+ const settingsEl = $('#settings-content')
+ settingsEl.on('click', '#add-answer', () => {
+ addAnswer()
+ updateActionButtonStates()
+ })
+ settingsEl.on('change', '#multiple-response', function() {
+ multipleResponse = this.checked
+ refreshCorrectControls()
+ })
+ settingsEl.on('click', '.action-up', onActionUp)
+ settingsEl.on('click', '.action-down', onActionDown)
+ settingsEl.on('click', '.action-delete', onActionDelete)
+ }
+
+ const onLoad = async () => {
+ window.codioAssessmentsHelper.registerMessageListener(processMessage)
+ window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS)
+
+ bindEvents()
+ }
+
+ window.addEventListener('load', onLoad);
+})()
diff --git a/shuffle.js b/shuffle.js
new file mode 100644
index 0000000..beaf129
--- /dev/null
+++ b/shuffle.js
@@ -0,0 +1,42 @@
+const nativeFloor = Math.floor
+const seedrandom = Math.seedrandom
+
+function baseRandom(lower, upper, seed) {
+ const nativeRandom = seedrandom(seed)
+ return lower + nativeFloor(nativeRandom() * (upper - lower + 1))
+}
+
+function arrayShuffle(array, seed) {
+ return shuffleSelf(copyArray(array), undefined, seed)
+}
+
+function copyArray(source, array) {
+ let index = -1
+ const length = source.length
+
+ array || (array = Array(length))
+ while (++index < length) {
+ array[index] = source[index]
+ }
+ return array
+}
+
+function shuffleSelf(array, size, seed) {
+ let index = -1
+ const length = array.length
+ const lastIndex = length - 1
+
+ size = size === undefined ? length : size
+ while (++index < size) {
+ const rand = baseRandom(index, lastIndex, seed),
+ value = array[rand]
+
+ array[rand] = array[index]
+ array[index] = value
+ }
+ array.length = size
+ return array
+}
+
+window.multipleChoiceAssessment = window.multipleChoiceAssessment || {}
+window.multipleChoiceAssessment.shuffle = arrayShuffle