diff --git a/src/app/services/projectService.spec.ts b/src/app/services/projectService.spec.ts index f2b706d3501..0a2098fbb9f 100644 --- a/src/app/services/projectService.spec.ts +++ b/src/app/services/projectService.spec.ts @@ -7,6 +7,7 @@ import demoProjectJSON_import from './sampleData/curriculum/Demo.project.json'; import oneBranchTwoPathsProjectJSON_import from './sampleData/curriculum/OneBranchTwoPaths.project.json'; import scootersProjectJSON_import from './sampleData/curriculum/SelfPropelledVehiclesChallenge.project.json'; import twoStepsProjectJSON_import from './sampleData/curriculum/TwoSteps.project.json'; +import twoLesssonsProjectJSON_import from './sampleData/curriculum/TwoLessons.project.json'; import { PeerGrouping } from '../domain/peerGrouping'; import { StudentTeacherCommonServicesModule } from '../student-teacher-common-services.module'; import { EmbeddedContent } from '../../assets/wise5/components/embedded/EmbeddedContent'; @@ -25,6 +26,7 @@ let demoProjectJSON: any; let oneBranchTwoPathsProjectJSON: any; let scootersProjectJSON: any; let twoStepsProjectJSON: any; +let twoLessonsProjectJSON: any; describe('ProjectService', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -38,6 +40,7 @@ describe('ProjectService', () => { oneBranchTwoPathsProjectJSON = copy(oneBranchTwoPathsProjectJSON_import); scootersProjectJSON = copy(scootersProjectJSON_import); twoStepsProjectJSON = copy(twoStepsProjectJSON_import); + twoLessonsProjectJSON = copy(twoLesssonsProjectJSON_import); }); shouldReplaceAssetPathsInNonHtmlComponentContent(); shouldReplaceAssetPathsInHtmlComponentContent(); @@ -434,6 +437,7 @@ function calculateNodeNumbers(): void { calculateNodeNumbersWhenNoBranches(); calculateNodeNumbersWhenBranchInOneActivity(); calculateNodeNumbersWhenBranchSpansMultipleActivities(); + calculateNodeNumbersWhenOnlyLessons(); } function calculateNodeNumbersWhenNoBranches(): void { @@ -442,6 +446,7 @@ function calculateNodeNumbersWhenNoBranches(): void { service.project = twoStepsProjectJSON; service.parseProject(); expectNodeIdsToHaveNumbers([ + { nodeId: 'group1', number: '1' }, { nodeId: 'node1', number: '1.1' }, { nodeId: 'node2', number: '1.2' } ]); @@ -455,6 +460,7 @@ function calculateNodeNumbersWhenBranchInOneActivity(): void { service.project = oneBranchTwoPathsProjectJSON; service.parseProject(); expectNodeIdsToHaveNumbers([ + { nodeId: 'group1', number: '1' }, { nodeId: 'node1', number: '1.1' }, { nodeId: 'node2', number: '1.2' }, { nodeId: 'node3', number: '1.3 A' }, @@ -474,9 +480,11 @@ function calculateNodeNumbersWhenBranchSpansMultipleActivities(): void { service.project = branchSpansActivitiesProjectJSON; service.parseProject(); expectNodeIdsToHaveNumbers([ + { nodeId: 'group1', number: '1' }, { nodeId: 'node1', number: '1.1' }, { nodeId: 'node2', number: '1.2 A' }, { nodeId: 'node3', number: '1.2 B' }, + { nodeId: 'group2', number: '2' }, { nodeId: 'node4', number: '2.1 A' }, { nodeId: 'node5', number: '2.1 B' }, { nodeId: 'node6', number: '2.2 B' }, @@ -486,6 +494,19 @@ function calculateNodeNumbersWhenBranchSpansMultipleActivities(): void { }); } +function calculateNodeNumbersWhenOnlyLessons(): void { + describe('project with only lessons', () => { + it('should calculate node numbers correctly', () => { + service.project = twoLessonsProjectJSON; + service.parseProject(); + expectNodeIdsToHaveNumbers([ + { nodeId: 'group1', number: '1' }, + { nodeId: 'group2', number: '2' } + ]); + }); + }); +} + function expectNodeIdsToHaveNumbers(objs: any[]): void { objs.forEach((obj: any) => { expectNodeIdToHaveNumber(obj.nodeId, obj.number); diff --git a/src/app/services/sampleData/curriculum/TwoLessons.project.json b/src/app/services/sampleData/curriculum/TwoLessons.project.json new file mode 100644 index 00000000000..0fc89b5ddca --- /dev/null +++ b/src/app/services/sampleData/curriculum/TwoLessons.project.json @@ -0,0 +1,143 @@ +{ + "nodes": [ + { + "id": "group0", + "type": "group", + "title": "Master", + "startId": "group1", + "ids": [ + "group1", + "group2" + ] + }, + { + "id": "group1", + "type": "group", + "title": "Lesson 1", + "startId": "", + "constraints": [], + "transitionLogic": { + "transitions": [ + { + "to": "group2" + } + ] + }, + "ids": [] + }, + { + "id": "group2", + "type": "group", + "title": "Lesson 2", + "startId": "", + "constraints": [], + "transitionLogic": { + "transitions": [] + }, + "ids": [] + } + ], + "constraints": [], + "startGroupId": "group0", + "startNodeId": "group1", + "navigationMode": "guided", + "layout": { + "template": "starmap|leftNav|rightNav" + }, + "metadata": { + "title": "Node Number Test", + "authors": [ + { + "firstName": "g", + "lastName": "k", + "id": 3, + "username": "gk" + } + ] + }, + "notebook": { + "enabled": false, + "label": "Notebook", + "enableAddNew": true, + "itemTypes": { + "note": { + "type": "note", + "enabled": true, + "enableLink": true, + "enableAddNote": true, + "enableClipping": true, + "enableStudentUploads": true, + "requireTextOnEveryNote": false, + "label": { + "singular": "note", + "plural": "notes", + "link": "Notes", + "icon": "note", + "color": "#1565C0" + } + }, + "report": { + "enabled": false, + "label": { + "singular": "report", + "plural": "reports", + "link": "Report", + "icon": "assignment", + "color": "#AD1457" + }, + "notes": [ + { + "reportId": "finalReport", + "title": "Final Report", + "description": "Final summary report of what you learned in this unit", + "prompt": "Use this space to write your final report using evidence from your notebook.", + "content": "

This is a heading

This is a paragraph.

" + } + ] + } + } + }, + "teacherNotebook": { + "enabled": true, + "label": "Teacher Notebook", + "enableAddNew": true, + "itemTypes": { + "note": { + "type": "note", + "enabled": false, + "enableLink": true, + "enableAddNote": true, + "enableClipping": true, + "enableStudentUploads": true, + "requireTextOnEveryNote": false, + "label": { + "singular": "note", + "plural": "notes", + "link": "Notes", + "icon": "note", + "color": "#1565C0" + } + }, + "report": { + "enabled": true, + "label": { + "singular": "teacher notes", + "plural": "teacher notes", + "link": "Teacher Notes", + "icon": "assignment", + "color": "#AD1457" + }, + "notes": [ + { + "reportId": "teacherReport", + "title": "Teacher Notes", + "description": "Notes for the teacher as they're running the WISE unit", + "prompt": "Use this space to take notes for this unit", + "content": "

Use this space to take notes for this unit

" + } + ] + } + } + }, + "inactiveNodes": [] +} \ No newline at end of file diff --git a/src/assets/wise5/services/projectService.ts b/src/assets/wise5/services/projectService.ts index ecf2d6094df..915dc09ce0b 100644 --- a/src/assets/wise5/services/projectService.ts +++ b/src/assets/wise5/services/projectService.ts @@ -1444,310 +1444,307 @@ export class ProjectService { } /** - * Recursively calcualte the node numbers by traversing the project tree - * using transitions + * Recursively calculate the node numbers by traversing the project tree using transitions * @param nodeId the current node id we are on * @param currentActivityNumber the current activity number * @param currentStepNumber the current step number - * @param branchLetterCode (optional) the character code for the branch - * letter e.g. 1=A, 2=B, etc. + * @param branchLetterCode (optional) the character code for the branch letter e.g. 0=A, 1=B, etc. */ - calculateNodeNumbersHelper( + private calculateNodeNumbersHelper( nodeId: string, currentActivityNumber: any, currentStepNumber: number, - branchLetterCode = null + branchLetterCode: number = null ): number { - if (nodeId != null) { - if (this.isApplicationNode(nodeId)) { - const node = this.getNodeById(nodeId); - if (node != null) { - const parentGroup = this.getParentGroup(nodeId); - if (parentGroup != null) { - if (this.nodeIdToNumber[parentGroup.id] == null) { - /* - * the parent group has not been assigned a number so - * we will assign a number now - */ - - currentActivityNumber = parseInt(currentActivityNumber) + 1; - - /* - * set the current step number to 1 now that we have - * entered a new group - */ - currentStepNumber = 1; - - this.nodeIdToNumber[parentGroup.id] = '' + currentActivityNumber; - } else { - /* - * the parent group has previously been assigned a number so we - * will use it - */ - currentActivityNumber = this.nodeIdToNumber[parentGroup.id]; - } - } - - if (this.isBranchMergePoint(nodeId)) { - /* - * the node is a merge point so we will not use a letter - * anymore now that we are no longer in a branch path - */ - branchLetterCode = null; - } - - if (this.isBranchStartPoint(nodeId)) { - const branchesByBranchStartPointNodeId = this.getBranchesByBranchStartPointNodeId( - nodeId - ); - const branches = branchesByBranchStartPointNodeId[0]; - - /* - * been used in the branch paths so that we know what - * step number to give the merge end point - * this is used to obtain the max step number that has - */ - let maxCurrentStepNumber = 0; - - // set the step number for the branch start point - this.nodeIdToNumber[nodeId] = currentActivityNumber + '.' + currentStepNumber; - - currentStepNumber++; - const branchPaths = branches.paths; - - for (let bp = 0; bp < branchPaths.length; bp++) { - const branchPath = branchPaths[bp]; - let branchCurrentStepNumber = currentStepNumber; - - // get the letter code e.g. 1=A, 2=B, etc. - const branchLetterCode = bp; - - for (let bpn = 0; bpn < branchPath.length; bpn++) { - if (bpn == 0) { - if (this.getParentGroupId(nodeId) !== this.getParentGroupId(branchPath[bpn])) { - branchCurrentStepNumber = 1; - } - - /* - * Recursively call calculateNodeNumbersHelper on the - * first step in this branch path. This will recursively - * calculate the numbers for all the nodes in this - * branch path. - */ - const branchPathNodeId = branchPath[bpn]; - branchCurrentStepNumber = this.calculateNodeNumbersHelper( - branchPathNodeId, - currentActivityNumber, - branchCurrentStepNumber, - branchLetterCode - ); - } - - /* - * update the max current step number if we have found - * a larger number - */ - if (branchCurrentStepNumber > maxCurrentStepNumber) { - maxCurrentStepNumber = branchCurrentStepNumber; - } - } - } - - // get the step number we should use for the end point - currentStepNumber = maxCurrentStepNumber; - - const branchEndPointNodeId = branches.endPoint; - - /* - * calculate the node number for the branch end point and - * continue calculating node numbers for the nodes that - * come after it - */ - this.calculateNodeNumbersHelper( - branchEndPointNodeId, - currentActivityNumber, - currentStepNumber - ); - } else { - // the node is not a branch start point - - /* - * check if we have already set the number for this node so - * that we don't need to unnecessarily re-calculate the - * node number - */ - if (this.nodeIdToNumber[nodeId] == null) { - // we have not calculated the node number yet - - let number = null; - - if (branchLetterCode == null) { - // we do not need to add a branch letter - - // get the node number e.g. 1.5 - number = currentActivityNumber + '.' + currentStepNumber; - } else { - // we need to add a branch letter - - // get the branch letter - const branchLetter = String.fromCharCode(65 + branchLetterCode); - - // get the node number e.g. 1.5 A - number = currentActivityNumber + '.' + currentStepNumber + ' ' + branchLetter; - - // remember the branch path letter for this node - this.nodeIdToBranchPathLetter[nodeId] = branchLetter; - } + if (this.isApplicationNode(nodeId)) { + currentStepNumber = this.calculateNodeNumberForStep( + nodeId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } else { + currentStepNumber = this.calculateNodeNumberForLesson( + nodeId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } + return currentStepNumber; + } - // set the number for the node - this.nodeIdToNumber[nodeId] = number; - } else { - /* - * We have calculated the node number before so we - * will return. This will prevent infinite looping - * within the project. - */ - return; - } + private calculateNodeNumberForStep( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number = null + ): number { + ({ currentActivityNumber, currentStepNumber, branchLetterCode } = this.getCurrentActivityNumber( + nodeId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + )); + if (this.isBranchStartPoint(nodeId)) { + currentStepNumber = this.calculateNodeNumberForBranchStartPoint( + nodeId, + currentActivityNumber, + currentStepNumber + ); + } else { + currentStepNumber = this.calculateNodeNumberForRegularStep( + nodeId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } + return currentStepNumber; + } - // increment the step number for the next node to use - currentStepNumber++; + private getCurrentActivityNumber( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number = null + ): any { + const parentGroup = this.getParentGroup(nodeId); + if (parentGroup != null) { + if (this.nodeIdToNumber[parentGroup.id] == null) { + // the parent group has not been assigned a number so we will assign a number now + currentActivityNumber = parseInt(currentActivityNumber) + 1; - let transitions = []; + // set the current step number to 1 now that we have entered a new group + currentStepNumber = 1; + this.nodeIdToNumber[parentGroup.id] = '' + currentActivityNumber; + } else { + // the parent group has previously been assigned a number so we will use it + currentActivityNumber = this.nodeIdToNumber[parentGroup.id]; + } + } + if (this.isBranchMergePoint(nodeId)) { + // the node is a merge point so we will not use a letter anymore now that we are no longer in + // a branch path + branchLetterCode = null; + } + return { currentActivityNumber, currentStepNumber, branchLetterCode }; + } - if (node.transitionLogic != null && node.transitionLogic.transitions) { - transitions = node.transitionLogic.transitions; - } + private calculateNodeNumberForBranchStartPoint( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number + ): number { + this.nodeIdToNumber[nodeId] = currentActivityNumber + '.' + currentStepNumber; + currentStepNumber++; + let maxCurrentStepNumber = 0; + const branchesByBranchStartPointNodeId = this.getBranchesByBranchStartPointNodeId(nodeId); + const branches = branchesByBranchStartPointNodeId[0]; + const branchPaths = branches.paths; + for (let branchPathNumber = 0; branchPathNumber < branchPaths.length; branchPathNumber++) { + maxCurrentStepNumber = this.calculateBranchPathNodeNumbers( + nodeId, + currentActivityNumber, + currentStepNumber, + branchPaths[branchPathNumber], + branchPathNumber, + maxCurrentStepNumber + ); + } + currentStepNumber = maxCurrentStepNumber; + this.calculateNodeNumbersHelper(branches.endPoint, currentActivityNumber, currentStepNumber); + return currentStepNumber; + } - if (transitions.length > 0) { - /* - * loop through all the transitions, there should only - * be one but we will loop through them just to be complete. - * if there was more than one transition, it would mean - * this node is a branch start point in which case we - * would have gone inside the other block of code where - * this.isBranchStartPoint() is true. - */ - for (let transition of transitions) { - if (transition != null) { - if (this.isBranchMergePoint(transition.to)) { - } else { - if (this.getParentGroupId(nodeId) !== this.getParentGroupId(transition.to)) { - currentStepNumber = 1; - } - currentStepNumber = this.calculateNodeNumbersHelper( - transition.to, - currentActivityNumber, - currentStepNumber, - branchLetterCode - ); - } - } - } - } else { - // if there are no transitions, check if the parent group has a transition - - if ( - parentGroup != null && - parentGroup.transitionLogic != null && - parentGroup.transitionLogic.transitions != null && - parentGroup.transitionLogic.transitions.length > 0 - ) { - for (let transition of parentGroup.transitionLogic.transitions) { - if (transition != null) { - this.calculateNodeNumbersHelper( - transition.to, - currentActivityNumber, - currentStepNumber, - branchLetterCode - ); - } - } - } - } - } + private calculateBranchPathNodeNumbers( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchPath: any, + branchPathNumber: number, + maxCurrentStepNumber: number + ): number { + // get the letter code e.g. 0=A, 1=B, etc. + const branchLetterCode = branchPathNumber; + let branchCurrentStepNumber = currentStepNumber; + for (let bpn = 0; bpn < branchPath.length; bpn++) { + if (bpn == 0) { + if (this.getParentGroupId(nodeId) !== this.getParentGroupId(branchPath[bpn])) { + branchCurrentStepNumber = 1; } - } else { - // the node is a group node + // Calculate the node numbers for the steps in this branch path + const branchPathNodeId = branchPath[bpn]; + branchCurrentStepNumber = this.calculateNodeNumbersHelper( + branchPathNodeId, + currentActivityNumber, + branchCurrentStepNumber, + branchLetterCode + ); + } + if (branchCurrentStepNumber > maxCurrentStepNumber) { + maxCurrentStepNumber = branchCurrentStepNumber; + } + } + return maxCurrentStepNumber; + } - const node = this.getNodeById(nodeId); - if (node != null) { - // check if the group has previously been assigned a number - if (this.nodeIdToNumber[nodeId] == null) { - /* - * the group has not been assigned a number so - * we will assign a number now - */ - if (nodeId == 'group0') { - // group 0 will always be given the activity number of 0 - this.nodeIdToNumber[nodeId] = '' + 0; - } else { - // set the activity number - currentActivityNumber = parseInt(currentActivityNumber) + 1; + private calculateNodeNumberForRegularStep( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number = null + ): number { + // Check if we have already set the number for this node so that we don't need to unnecessarily + // re-calculate the node number + if (this.nodeIdToNumber[nodeId] == null) { + this.setStepNodeNumber(nodeId, currentActivityNumber, currentStepNumber, branchLetterCode); + } else { + // We have calculated the node number before so we will return. + // This will prevent infinite looping within the project. + return currentStepNumber; + } + currentStepNumber++; + const node = this.getNodeById(nodeId); + const transitions = node.transitionLogic.transitions; + if (transitions.length > 0) { + currentStepNumber = this.calculateNodeNumberForRegularStepThatHasTransitions( + nodeId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } else { + currentStepNumber = this.calculateNodeNumberForRegularStepThatDoesNotHaveTransitions( + nodeId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } + return currentStepNumber; + } - /* - * set the current step number to 1 now that we have - * entered a new group - */ - currentStepNumber = 1; + private calculateNodeNumberForRegularStepThatHasTransitions( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number = null + ) { + const node = this.getNodeById(nodeId); + for (const transition of node.transitionLogic.transitions) { + if (!this.isBranchMergePoint(transition.to)) { + if (this.getParentGroupId(nodeId) !== this.getParentGroupId(transition.to)) { + currentStepNumber = 1; + } + currentStepNumber = this.calculateNodeNumbersHelper( + transition.to, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } + } + return currentStepNumber; + } - // set the activity number - this.nodeIdToNumber[nodeId] = '' + currentActivityNumber; - } - } else { - /* - * We have calculated the node number before so we - * will return. This will prevent infinite looping - * within the project. - */ - return; - } + private calculateNodeNumberForRegularStepThatDoesNotHaveTransitions( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number = null + ): number { + const parentGroup = this.getParentGroup(nodeId); + if (parentGroup.transitionLogic?.transitions != null) { + for (const transition of parentGroup.transitionLogic.transitions) { + currentStepNumber = this.calculateNodeNumbersHelper( + transition.to, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } + } + return currentStepNumber; + } - if (node.startId != null && node.startId != '') { - /* - * calculate the node number for the first step in this - * activity and any steps after it - */ - this.calculateNodeNumbersHelper( - node.startId, - currentActivityNumber, - currentStepNumber, - branchLetterCode - ); - } else { - /* - * this activity doesn't have a start step so we will - * look for a transition - */ + private setStepNodeNumber( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number + ) { + let number = null; + if (branchLetterCode == null) { + number = currentActivityNumber + '.' + currentStepNumber; + } else { + const branchLetter = String.fromCharCode(65 + branchLetterCode); + number = `${currentActivityNumber}.${currentStepNumber} ${branchLetter}`; + this.nodeIdToBranchPathLetter[nodeId] = branchLetter; + } + this.nodeIdToNumber[nodeId] = number; + } - if ( - node != null && - node.transitionLogic != null && - node.transitionLogic.transitions != null && - node.transitionLogic.transitions.length > 0 - ) { - for (let transition of node.transitionLogic.transitions) { - if (transition != null) { - /* - * calculate the node number for the next group - * and all its children steps - */ - this.calculateNodeNumbersHelper( - transition.to, - currentActivityNumber, - currentStepNumber, - branchLetterCode - ); - } - } - } - } - } + private calculateNodeNumberForLesson( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number, + branchLetterCode: number = null + ): number { + // check if the group has previously been assigned a number + if (this.nodeIdToNumber[nodeId] == null) { + ({ currentActivityNumber, currentStepNumber } = this.setLessonNodeNumber( + nodeId, + currentActivityNumber, + currentStepNumber + )); + } else { + // We have calculated the node number before so we will return. + // This will prevent infinite looping within the project. + return currentStepNumber; + } + const node = this.getNodeById(nodeId); + if (node.startId != '') { + // calculate the node number for the first step in this activity and any steps after it + this.calculateNodeNumbersHelper( + node.startId, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); + } else { + // this activity doesn't have a start step so we will look for a transition + for (const transition of node.transitionLogic.transitions) { + // calculate the node number for the next node and all its children steps + this.calculateNodeNumbersHelper( + transition.to, + currentActivityNumber, + currentStepNumber, + branchLetterCode + ); } } return currentStepNumber; } + private setLessonNodeNumber( + nodeId: string, + currentActivityNumber: any, + currentStepNumber: number + ): any { + // the group has not been assigned a number so we will assign a number now/ + if (nodeId == 'group0') { + // group 0 will always be given the activity number of 0 + this.nodeIdToNumber[nodeId] = '0'; + } else { + // set the current step number to 1 now that we have entered a new group + currentStepNumber = 1; + currentActivityNumber = parseInt(currentActivityNumber) + 1; + this.nodeIdToNumber[nodeId] = '' + currentActivityNumber; + } + return { currentActivityNumber, currentStepNumber }; + } + getProjectScript(): any { return this.project.script; }