update requirements on submit

closes LF-613
flag=differentiated_modules
qa risk: low

test plan:
-have a module with some module items
-open the tray via assign to and switch to settings
-scroll down to requirements and change the count
 and some of the content settings
>click submit and confirm the page updates
>open the tray again and confirm it parses the
 requirements properly
-refresh the page
>make sure the requirements match what you had
 previously entered
-open the tray and clear our the requirements
>click submit and confirm the requirements are
 removed in the UI

Change-Id: I5e20eb430d250db00e1c9609e2dd4d51dbe25c3f
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/328736
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Robin Kuss <rkuss@instructure.com>
QA-Review: Robin Kuss <rkuss@instructure.com>
Product-Review: Jake Oeding <jake.oeding@instructure.com>
This commit is contained in:
jake.oeding 2023-09-26 17:29:25 -04:00 committed by Jake Oeding
parent ee75c847da
commit 8708e837b1
3 changed files with 181 additions and 1 deletions

View File

@ -23,6 +23,7 @@ import {
convertFriendlyDatetimeToUTC,
convertModuleSettingsForApi,
} from '../miscHelpers'
import type {Requirement} from '../../react/types'
describe('calculatePanelHeight', () => {
it('computes the correct height when withinTabs is true', () => {
@ -63,6 +64,29 @@ describe('convertModuleSettingsForApi', () => {
{id: '1', name: 'Week 1'},
{id: '2', name: 'Week 2'},
],
requirements: [
{type: 'view', id: '1', name: 'Mod 1', resource: 'externalUrl'},
{type: 'mark', id: '2', name: 'Mod 2', resource: 'page'},
{
type: 'submit',
id: '3',
name: 'Mod 3',
resource: 'assignment',
minimumScore: '50',
pointsPossible: '100',
},
{
type: 'score',
id: '4',
name: 'Mod 4',
resource: 'quiz',
minimumScore: '50',
pointsPossible: '100',
},
{type: 'contribute', id: '5', name: 'Mod 5', resource: 'discussion'},
] as Requirement[],
requirementCount: 'all' as const,
requireSequentialProgress: false,
}
it('converts the module settings to the format expected by the API', () => {
@ -71,6 +95,15 @@ describe('convertModuleSettingsForApi', () => {
name: 'Module 1',
unlock_at: '2023-08-02T06:00:00.000Z',
prerequisites: 'module_1,module_2',
completion_requirements: {
1: {min_score: '', type: 'must_view'},
2: {min_score: '', type: 'must_mark_done'},
3: {min_score: '', type: 'must_submit'},
4: {min_score: '50', type: 'min_score'},
5: {min_score: '', type: 'must_contribute'},
},
requirement_count: '',
require_sequential_progress: false,
},
})
})
@ -82,4 +115,30 @@ describe('convertModuleSettingsForApi', () => {
})
expect(formattedSettings.context_module.unlock_at).toBe(null)
})
it('has requirement_count of 1 if requirementCount is one', () => {
const formattedSettings = convertModuleSettingsForApi({
...moduleSettings,
requirementCount: 'one',
})
expect(formattedSettings.context_module.requirement_count).toBe('1')
})
it('has require_sequential_progress of true if count is all and RSP is true', () => {
const formattedSettings = convertModuleSettingsForApi({
...moduleSettings,
requirementCount: 'all',
requireSequentialProgress: true,
})
expect(formattedSettings.context_module.require_sequential_progress).toBe(true)
})
it('has requirem_sequential_progress of false if count is one and RSP is true', () => {
const formattedSettings = convertModuleSettingsForApi({
...moduleSettings,
requirementCount: 'one',
requireSequentialProgress: true,
})
expect(formattedSettings.context_module.require_sequential_progress).toBe(false)
})
})

View File

@ -33,6 +33,14 @@ export function convertFriendlyDatetimeToUTC(date: string | null | undefined): s
}
export function convertModuleSettingsForApi(moduleSettings: SettingsPanelState) {
const typeMap: Record<Requirement['type'], string> = {
view: 'must_view',
mark: 'must_mark_done',
submit: 'must_submit',
score: 'min_score',
contribute: 'must_contribute',
}
return {
context_module: {
name: moduleSettings.moduleName,
@ -41,6 +49,16 @@ export function convertModuleSettingsForApi(moduleSettings: SettingsPanelState)
.filter(prerequisite => prerequisite.id !== '-1')
.map(prerequisite => `module_${prerequisite.id}`)
.join(','),
completion_requirements: moduleSettings.requirements.reduce((requirements, requirement) => {
requirements[requirement.id] = {
type: typeMap[requirement.type],
min_score: requirement.type === 'score' ? requirement.minimumScore : '',
}
return requirements
}, {} as Record<string, Record<string, string>>),
requirement_count: moduleSettings.requirementCount === 'one' ? '1' : '',
require_sequential_progress:
moduleSettings.requirementCount === 'all' && moduleSettings.requireSequentialProgress,
},
}
}

View File

@ -43,6 +43,39 @@ const requirementTypeMap: Record<string, Requirement['type']> = {
must_submit_requirement: 'submit',
}
const requirementTypeMapReverse: Record<Requirement['type'], string> = {
score: 'min_score_requirement',
view: 'must_view_requirement',
mark: 'must_mark_done_requirement',
contribute: 'must_contribute_requirement',
submit: 'must_submit_requirement',
}
const requirementFriendlyLabelMap: Record<Requirement['type'], string> = {
score: I18n.t('Score at least'),
view: I18n.t('View'),
mark: I18n.t('Mark done'),
contribute: I18n.t('Contribute'),
submit: I18n.t('Submit'),
}
function requirementScreenreaderMessage(requirement: Requirement) {
switch (requirement.type) {
case 'score':
return I18n.t('Must score at least %{points} to complete this module item', {
points: requirement.minimumScore,
})
case 'view':
return I18n.t('Must view in order to complete this module item')
case 'mark':
return I18n.t('Must mark this module item done in order to complete')
case 'contribute':
return I18n.t('Must contribute to this module item to complete it')
case 'submit':
return I18n.t('Must submit this module item to complete it')
}
}
export function parseModule(element: HTMLDivElement) {
const moduleId = element.getAttribute('data-module-id')
const moduleName = element.querySelector('.name')?.getAttribute('title') ?? ''
@ -144,7 +177,7 @@ function parseModuleItems(element: HTMLDivElement) {
}
export function updateModuleUI(moduleElement: HTMLDivElement, moduleSettings: SettingsPanelState) {
;[updateName, updateUnlockTime, updatePrerequisites].forEach(fn =>
;[updateName, updateUnlockTime, updatePrerequisites, updateRequirements].forEach(fn =>
fn(moduleElement, moduleSettings)
)
}
@ -267,3 +300,73 @@ function updatePrerequisites(moduleElement: HTMLDivElement, moduleSettings: Sett
prerequisiteElement.appendChild(prereqMessageElement)
}
}
function updateRequirements(moduleElement: HTMLDivElement, moduleSettings: SettingsPanelState) {
const requirementsMessageElement = moduleElement.querySelector('.requirements_message')
if (requirementsMessageElement) {
requirementsMessageElement.setAttribute(
'data-requirement-type',
moduleSettings.requirementCount
)
if (moduleSettings.requirements.length === 0) {
requirementsMessageElement.innerHTML = ``
} else {
const requirementText =
moduleSettings.requirementCount === 'all' ? 'Complete All Items' : 'Complete One Item'
requirementsMessageElement.innerHTML = `
<ul class="pill">
<li aria-label="${requirementText}">${requirementText}</li>
</ul>
`
}
}
const sequentialProgressElement = moduleElement.querySelector('.require_sequential_progress')
if (sequentialProgressElement) {
sequentialProgressElement.textContent = moduleSettings.requireSequentialProgress.toString()
}
// Clear everything out so we can start with a fresh slate
moduleElement.querySelectorAll('.ig-row').forEach(item => {
item.classList.remove('with-completion-requirements')
const descriptionElement = item.querySelector('.requirement-description')
if (descriptionElement) {
descriptionElement.innerHTML = ''
}
})
moduleSettings.requirements.forEach(requirement => {
const moduleItemElement = moduleElement.querySelector(`#context_module_item_${requirement.id}`)
if (moduleItemElement) {
moduleItemElement.querySelector('.ig-row')?.classList.add('with-completion-requirements')
const descriptionElement = moduleItemElement.querySelector('.requirement-description')
if (descriptionElement) {
const scoreElement =
requirement.type === 'score'
? `<span class="min_score"> ${requirement.minimumScore}</span>`
: ''
descriptionElement.innerHTML = `
<span class="requirement_type ${requirementTypeMapReverse[requirement.type]}">
<span class="unfulfilled">
${requirementFriendlyLabelMap[requirement.type]}${scoreElement}
<span class="screenreader-only">${requirementScreenreaderMessage(requirement)}</span>
</span>
</span>
`
}
const pointsPossibleElement = moduleItemElement.querySelector('.points_possible_display')
if (pointsPossibleElement) {
if (requirement.type === 'score' && requirement.pointsPossible) {
pointsPossibleElement.textContent = I18n.t('%{points} pts', {
points: I18n.n(requirement.pointsPossible),
})
} else {
pointsPossibleElement.textContent = ''
}
}
}
})
}