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:
parent
ee75c847da
commit
8708e837b1
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue