From 7d6de50a05ae78b9e729666bdd705a8b90bb3cc8 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Tue, 11 Apr 2023 10:22:27 -0700 Subject: [PATCH] refactor(MilestoneService): Extract criteria evaluator (#1174) Also fix Milestone dialog widths Co-authored-by: Jonathan Lim-Breitbart --- .../milestones/milestones.component.ts | 2 +- src/app/services/milestoneService.spec.ts | 292 +----------------- .../sample_aggregateAutoScores.json | 15 + .../sampleData/sample_satisfyCriterion.json | 10 + .../node-grading-view.component.ts | 3 +- .../milestoneCriteriaEvaluator.spec.ts | 138 +++++++++ .../milestones/milestoneCriteriaEvaluator.ts | 128 ++++++++ src/assets/wise5/services/milestoneService.ts | 152 +-------- 8 files changed, 309 insertions(+), 431 deletions(-) create mode 100644 src/app/services/sampleData/sample_aggregateAutoScores.json create mode 100644 src/app/services/sampleData/sample_satisfyCriterion.json create mode 100644 src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.spec.ts create mode 100644 src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.ts diff --git a/src/app/classroom-monitor/milestones/milestones.component.ts b/src/app/classroom-monitor/milestones/milestones.component.ts index 6f2a12fdaf5..f81061582f6 100644 --- a/src/app/classroom-monitor/milestones/milestones.component.ts +++ b/src/app/classroom-monitor/milestones/milestones.component.ts @@ -84,7 +84,7 @@ export class MilestonesComponent { showMilestoneDetails(milestone: any): void { this.dialog.open(MilestoneDetailsDialogComponent, { data: milestone, - width: '1280px' + panelClass: 'dialog-lg' }); } } diff --git a/src/app/services/milestoneService.spec.ts b/src/app/services/milestoneService.spec.ts index 244365f4ca3..1ea34385eb7 100644 --- a/src/app/services/milestoneService.spec.ts +++ b/src/app/services/milestoneService.spec.ts @@ -7,67 +7,20 @@ import { AchievementService } from '../../assets/wise5/services/achievementServi import { ConfigService } from '../../assets/wise5/services/configService'; import { ProjectService } from '../../assets/wise5/services/projectService'; import { TeacherDataService } from '../../assets/wise5/services/teacherDataService'; -import { UtilService } from '../../assets/wise5/services/utilService'; import { TeacherProjectService } from '../../assets/wise5/services/teacherProjectService'; import { TeacherWebSocketService } from '../../assets/wise5/services/teacherWebSocketService'; import { ClassroomStatusService } from '../../assets/wise5/services/classroomStatusService'; import { CopyNodesService } from '../../assets/wise5/services/copyNodesService'; import { MatDialogModule } from '@angular/material/dialog'; import { StudentTeacherCommonServicesModule } from '../student-teacher-common-services.module'; +import aggregateAutoScoresSample from './sampleData/sample_aggregateAutoScores.json'; +import satisfyCriterionSample from './sampleData/sample_satisfyCriterion.json'; let service: MilestoneService; let achievementService: AchievementService; let configService: ConfigService; let projectService: ProjectService; let teacherDataService: TeacherDataService; -let utilService: UtilService; - -const satisfyCriterionSample = { - percentThreshold: 50, - targetVariable: 'ki', - componentId: 'xfns1g7pga', - function: 'percentOfScoresNotEqualTo', - id: 'template1SatisfyCriteria0', - type: 'autoScore', - nodeId: 'node1', - value: 3 -}; - -const aggregateAutoScoresSample = [ - { - nodeId: 'node1', - componentId: 'xfns1g7pga', - stepTitle: 'Step 1.1: Hello', - aggregateAutoScore: { - ki: { - counts: { 1: 2, 2: 0, 3: 1, 4: 0, 5: 0 }, - scoreSum: 5, - scoreCount: 3, - average: 1.67 - } - } - } -]; - -const possibleScoresKi = [1, 2, 3, 4, 5]; - -const sampleAggregateData = { - counts: createScoreCounts([10, 20, 30, 40, 50]) -}; - -const aggregateAutoScores50 = [ - { - nodeId: 'node1', - componentId: 'component1', - stepTitle: 'Step 1.2: World', - aggregateAutoScore: { - ki: { - counts: createScoreCounts([10, 10, 10, 10, 10]), - scoreCount: 50 - } - } - } -]; const reportSettingsCustomScoreValuesSample = { customScoreValues: { @@ -98,10 +51,8 @@ describe('MilestoneService', () => { configService = TestBed.inject(ConfigService); projectService = TestBed.inject(ProjectService); teacherDataService = TestBed.inject(TeacherDataService); - utilService = TestBed.inject(UtilService); }); getProjectMilestones(); - getProjectMilestoneReports(); getMilestoneReportByNodeId(); getProjectMilestoneStatus(); insertMilestoneItems(); @@ -112,10 +63,6 @@ describe('MilestoneService', () => { isCompletionReached(); generateReport(); chooseTemplate(); - isTemplateMatch(); - isTemplateCriterionSatisfied(); - getAggregateData(); - getPossibleScores(); getSatisfyCriteriaReferencedComponents(); adjustKIScore(); getKIScoreBounds(); @@ -179,27 +126,6 @@ function getProjectMilestones() { }); } -function getProjectMilestoneReports() { - describe('getProjectMilestoneReports()', () => { - it('should get project milestone reports', () => { - const achievements = { - isEnabled: true, - items: [ - { - type: 'milestone' - }, - { - type: 'milestoneReport' - } - ] - }; - spyOn(projectService, 'getAchievements').and.returnValue(achievements); - const milestoneReports = service.getProjectMilestoneReports(); - expect(milestoneReports.length).toEqual(1); - }); - }); -} - function getMilestoneReportByNodeId() { describe('getMilestoneReportByNodeId()', () => { it('should get project milestone report by node id when there is none', () => { @@ -565,225 +491,13 @@ function chooseTemplate() { const templates = [template1, template2]; const aggregateAutoScores = []; spyOn(service, 'isTemplateMatch').and.callFake((template, aggregateAutoScores) => { - if (template.id === 'template-1') { - return false; - } else if (template.id === 'template-2') { - return true; - } + return template.id === 'template-2'; }); expect(service.chooseTemplate(templates, aggregateAutoScores)).toEqual(template2); }); }); } -function isTemplateMatch() { - describe('isTemplateMatch()', () => { - const aggregateAutoScores = []; - const satisfyCriteria = [ - { - id: 'satisfy-criteria-1' - }, - { - id: 'satisfy-criteria-2' - } - ]; - it('should check is template match with all conditional false', () => { - const template = { - satisfyConditional: 'all', - satisfyCriteria: satisfyCriteria - }; - spyOn(service, 'isTemplateCriterionSatisfied').and.callFake( - (satisfyCriterion, aggregateAutoScores) => { - if (satisfyCriterion.id === 'satisfy-criteria-1') { - return false; - } else if (satisfyCriterion.id === 'satisfy-criteria-2') { - return true; - } - } - ); - expect(service.isTemplateMatch(template, aggregateAutoScores)).toEqual(false); - }); - it('should check is template match with all conditional true', () => { - const template = { - satisfyConditional: 'all', - satisfyCriteria: satisfyCriteria - }; - spyOn(service, 'isTemplateCriterionSatisfied').and.callFake( - (satisfyCriterion, aggregateAutoScores) => { - if (satisfyCriterion.id === 'satisfy-criteria-1') { - return true; - } else if (satisfyCriterion.id === 'satisfy-criteria-2') { - return true; - } - } - ); - expect(service.isTemplateMatch(template, aggregateAutoScores)).toEqual(true); - }); - it('should check is template match with any conditional false', () => { - const template = { - satisfyConditional: 'any', - satisfyCriteria: satisfyCriteria - }; - spyOn(service, 'isTemplateCriterionSatisfied').and.callFake( - (satisfyCriterion, aggregateAutoScores) => { - if (satisfyCriterion.id === 'satisfy-criteria-1') { - return false; - } else if (satisfyCriterion.id === 'satisfy-criteria-2') { - return false; - } - } - ); - expect(service.isTemplateMatch(template, aggregateAutoScores)).toEqual(false); - }); - it('should check is template match with any conditional true', () => { - const template = { - satisfyConditional: 'any', - satisfyCriteria: satisfyCriteria - }; - spyOn(service, 'isTemplateCriterionSatisfied').and.callFake( - (satisfyCriterion, aggregateAutoScores) => { - if (satisfyCriterion.id === 'satisfy-criteria-1') { - return false; - } else if (satisfyCriterion.id === 'satisfy-criteria-2') { - return true; - } - } - ); - expect(service.isTemplateMatch(template, aggregateAutoScores)).toEqual(true); - }); - }); -} - -function isTemplateCriterionSatisfied() { - it('should check is percent of scores greater than', () => { - const satisfyCriterion = { - function: 'percentOfScoresGreaterThan', - componentId: 'component1', - targetVariable: 'ki', - value: 3, - percentThreshold: 50 - }; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - false - ); - satisfyCriterion.value = 2; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - true - ); - }); - it('should check is percent of scores greater than or equal to', () => { - const satisfyCriterion = { - function: 'percentOfScoresGreaterThanOrEqualTo', - componentId: 'component1', - targetVariable: 'ki', - value: 4, - percentThreshold: 50 - }; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - false - ); - satisfyCriterion.value = 3; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - true - ); - }); - it('should check is percent of scores less than', () => { - const satisfyCriterion = { - function: 'percentOfScoresLessThan', - componentId: 'component1', - targetVariable: 'ki', - value: 3, - percentThreshold: 50 - }; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - false - ); - satisfyCriterion.value = 4; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - true - ); - }); - it('should check is percent of scores less than or equal to', () => { - const satisfyCriterion = { - function: 'percentOfScoresLessThanOrEqualTo', - componentId: 'component1', - targetVariable: 'ki', - value: 2, - percentThreshold: 50 - }; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - false - ); - satisfyCriterion.value = 3; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - true - ); - }); - it('should check is percent of scores equal to', () => { - const satisfyCriterion = { - function: 'percentOfScoresEqualTo', - componentId: 'component1', - targetVariable: 'ki', - value: 3, - percentThreshold: 50 - }; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual( - false - ); - const aggregateAutoScores = [ - { - nodeId: 'node1', - componentId: 'component1', - stepTitle: 'Step 1.1: Hello', - aggregateAutoScore: { - ki: { - counts: createScoreCounts([10, 0, 10, 0, 0]), - scoreCount: 20 - } - } - } - ]; - expect(service.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores)).toEqual( - true - ); - }); - it('should check is percent of scores not equal to', () => { - expect( - service.isTemplateCriterionSatisfied(satisfyCriterionSample, aggregateAutoScoresSample) - ).toEqual(true); - const aggregateAutoScores = angular.copy(aggregateAutoScoresSample); - aggregateAutoScores[0].aggregateAutoScore.ki.counts = { 1: 1, 2: 0, 3: 2, 4: 0, 5: 0 }; - expect( - service.isTemplateCriterionSatisfied(satisfyCriterionSample, aggregateAutoScores) - ).toEqual(false); - }); -} - -function getAggregateData() { - describe('getAggregateData()', () => { - it('should return the aggregate data', () => { - const result = service.getAggregateData(satisfyCriterionSample, aggregateAutoScoresSample); - expect(result).toEqual({ - counts: { 1: 2, 2: 0, 3: 1, 4: 0, 5: 0 }, - scoreCount: 3, - scoreSum: 5, - average: 1.67 - }); - }); - }); -} - -function getPossibleScores() { - describe('getPossibleScores()', () => { - const aggregateData = { - counts: { 2: 2, 1: 0, 3: 1, 4: 0, 5: 0 } - }; - it('should return the possible scores', () => { - expect(service.getPossibleScores(aggregateData)).toEqual([1, 2, 3, 4, 5]); - }); - }); -} - function getSatisfyCriteriaReferencedComponents() { describe('getSatisfyCriteriaReferencedComponents()', () => { it('should return referenced components', () => { diff --git a/src/app/services/sampleData/sample_aggregateAutoScores.json b/src/app/services/sampleData/sample_aggregateAutoScores.json new file mode 100644 index 00000000000..1cff6cf046a --- /dev/null +++ b/src/app/services/sampleData/sample_aggregateAutoScores.json @@ -0,0 +1,15 @@ +[ + { + "nodeId": "node1", + "componentId": "xfns1g7pga", + "stepTitle": "Step 1.1: Hello", + "aggregateAutoScore": { + "ki": { + "counts": { "1": 2, "2": 0, "3": 1, "4": 0, "5": 0 }, + "scoreSum": 5, + "scoreCount": 3, + "average": 1.67 + } + } + } +] \ No newline at end of file diff --git a/src/app/services/sampleData/sample_satisfyCriterion.json b/src/app/services/sampleData/sample_satisfyCriterion.json new file mode 100644 index 00000000000..76d7ba50812 --- /dev/null +++ b/src/app/services/sampleData/sample_satisfyCriterion.json @@ -0,0 +1,10 @@ +{ + "percentThreshold": 50, + "targetVariable": "ki", + "componentId": "xfns1g7pga", + "function": "percentOfScoresNotEqualTo", + "id": "template1SatisfyCriteria0", + "type": "autoScore", + "nodeId": "node1", + "value": 3 + } \ No newline at end of file diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.ts index 8ea4813e6cd..840c9677b1d 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.ts @@ -435,7 +435,8 @@ export class NodeGradingViewComponent implements OnInit { showReport(): void { this.dialog.open(MilestoneDetailsDialogComponent, { - data: this.milestoneReport + data: this.milestoneReport, + panelClass: 'dialog-lg' }); } diff --git a/src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.spec.ts b/src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.spec.ts new file mode 100644 index 00000000000..34b05f7438f --- /dev/null +++ b/src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.spec.ts @@ -0,0 +1,138 @@ +import { copy } from '../../common/object/object'; +import { MilestoneCriteriaEvaluator } from './milestoneCriteriaEvaluator'; +import aggregateAutoScoresSample from '../../../../app/services/sampleData/sample_aggregateAutoScores.json'; +import satisfyCriterionSample from '../../../../app/services/sampleData/sample_satisfyCriterion.json'; + +function createScoreCounts(counts: any[]): any { + const countsObject = {}; + for (let i = 0; i < counts.length; i++) { + countsObject[i + 1] = counts[i]; + } + return countsObject; +} + +const aggregateAutoScores50 = [ + { + nodeId: 'node1', + componentId: 'component1', + stepTitle: 'Step 1.2: World', + aggregateAutoScore: { + ki: { + counts: createScoreCounts([10, 10, 10, 10, 10]), + scoreCount: 50 + } + } + } +]; + +let evaluator: MilestoneCriteriaEvaluator = new MilestoneCriteriaEvaluator(); +describe('MilestoneCriteriaEvaluator', () => { + isSatisfied(); +}); + +function isSatisfied() { + describe('isSatisfied()', () => { + isSatisfied_percentOfScoresGreaterThan(); + isSatisfied_percentOfScoresGreaterThanOrEqualTo(); + isSatisfied_percentOfScoresLessThan(); + isSatisfied_percentOfScoresLessThanOrEqualTo(); + isSatisfied_percentOfScoresEqualTo(); + isSatisfied_percentOfScoresNotEqualTo(); + }); +} + +function isSatisfied_percentOfScoresGreaterThan() { + it('should check is percent of scores greater than', () => { + const satisfyCriterion = { + function: 'percentOfScoresGreaterThan', + componentId: 'component1', + targetVariable: 'ki', + value: 3, + percentThreshold: 50 + }; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(false); + satisfyCriterion.value = 2; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(true); + }); +} + +function isSatisfied_percentOfScoresGreaterThanOrEqualTo() { + it('should check is percent of scores greater than or equal to', () => { + const satisfyCriterion = { + function: 'percentOfScoresGreaterThanOrEqualTo', + componentId: 'component1', + targetVariable: 'ki', + value: 4, + percentThreshold: 50 + }; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(false); + satisfyCriterion.value = 3; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(true); + }); +} + +function isSatisfied_percentOfScoresLessThan() { + it('should check is percent of scores less than', () => { + const satisfyCriterion = { + function: 'percentOfScoresLessThan', + componentId: 'component1', + targetVariable: 'ki', + value: 3, + percentThreshold: 50 + }; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(false); + satisfyCriterion.value = 4; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(true); + }); +} + +function isSatisfied_percentOfScoresLessThanOrEqualTo() { + it('should check is percent of scores less than or equal to', () => { + const satisfyCriterion = { + function: 'percentOfScoresLessThanOrEqualTo', + componentId: 'component1', + targetVariable: 'ki', + value: 2, + percentThreshold: 50 + }; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(false); + satisfyCriterion.value = 3; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(true); + }); +} + +function isSatisfied_percentOfScoresEqualTo() { + it('should check is percent of scores equal to', () => { + const satisfyCriterion = { + function: 'percentOfScoresEqualTo', + componentId: 'component1', + targetVariable: 'ki', + value: 3, + percentThreshold: 50 + }; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores50)).toEqual(false); + const aggregateAutoScores = [ + { + nodeId: 'node1', + componentId: 'component1', + stepTitle: 'Step 1.1: Hello', + aggregateAutoScore: { + ki: { + counts: createScoreCounts([10, 0, 10, 0, 0]), + scoreCount: 20 + } + } + } + ]; + expect(evaluator.isSatisfied(satisfyCriterion, aggregateAutoScores)).toEqual(true); + }); +} + +function isSatisfied_percentOfScoresNotEqualTo() { + it('should check is percent of scores not equal to', () => { + expect(evaluator.isSatisfied(satisfyCriterionSample, aggregateAutoScoresSample)).toEqual(true); + const aggregateAutoScores = copy(aggregateAutoScoresSample); + aggregateAutoScores[0].aggregateAutoScore.ki.counts = { 1: 1, 2: 0, 3: 2, 4: 0, 5: 0 }; + expect(evaluator.isSatisfied(satisfyCriterionSample, aggregateAutoScores)).toEqual(false); + }); +} diff --git a/src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.ts b/src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.ts new file mode 100644 index 00000000000..e10942bd76c --- /dev/null +++ b/src/assets/wise5/classroomMonitor/milestones/milestoneCriteriaEvaluator.ts @@ -0,0 +1,128 @@ +export class MilestoneCriteriaEvaluator { + isSatisfied(satisfyCriterion: any, aggregateAutoScores: any[]): boolean { + return ( + satisfyCriterion.function === 'default' || + this.satisfyCriteriaFuncNameToFunc[satisfyCriterion.function]( + satisfyCriterion, + aggregateAutoScores + ) + ); + } + + private satisfyCriteriaFuncNameToFunc = { + percentOfScoresGreaterThan: (satisfyCriterion: any, aggregateAutoScores: any[]) => { + return this.isPercentOfScoresSatisfiesComparator( + satisfyCriterion, + aggregateAutoScores, + this.greaterThan + ); + }, + percentOfScoresGreaterThanOrEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { + return this.isPercentOfScoresSatisfiesComparator( + satisfyCriterion, + aggregateAutoScores, + this.greaterThanEqualTo + ); + }, + percentOfScoresLessThan: (satisfyCriterion: any, aggregateAutoScores: any[]) => { + return this.isPercentOfScoresSatisfiesComparator( + satisfyCriterion, + aggregateAutoScores, + this.lessThan + ); + }, + percentOfScoresLessThanOrEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { + return this.isPercentOfScoresSatisfiesComparator( + satisfyCriterion, + aggregateAutoScores, + this.lessThanEqualTo + ); + }, + percentOfScoresEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { + return this.isPercentOfScoresSatisfiesComparator( + satisfyCriterion, + aggregateAutoScores, + this.equalTo + ); + }, + percentOfScoresNotEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { + return this.isPercentOfScoresSatisfiesComparator( + satisfyCriterion, + aggregateAutoScores, + this.notEqualTo + ); + } + }; + + private isPercentOfScoresSatisfiesComparator( + satisfyCriterion: any, + aggregateAutoScores: any[], + comparator: any + ): boolean { + const aggregateData = this.getAggregateData(satisfyCriterion, aggregateAutoScores); + const possibleScores = this.getPossibleScores(aggregateData); + const sum = this.getComparatorSum(satisfyCriterion, aggregateData, possibleScores, comparator); + return this.isPercentThresholdSatisfied(satisfyCriterion, aggregateData, sum); + } + + private getAggregateData(satisfyCriterion: any, aggregateAutoScores: any[]) { + for (const aggregateAutoScore of aggregateAutoScores) { + if (aggregateAutoScore.componentId === satisfyCriterion.componentId) { + return aggregateAutoScore.aggregateAutoScore[satisfyCriterion.targetVariable]; + } + } + throw new Error(`Aggregate data not found for component ${satisfyCriterion.componentId}`); + } + + private getPossibleScores(aggregateData: any) { + return Object.keys(aggregateData.counts).map(Number).sort(); + } + + private getComparatorSum( + satisfyCriterion: any, + aggregateData: any, + possibleScores: number[], + comparator: any + ): number { + let sum = 0; + for (const possibleScore of possibleScores) { + if (comparator(possibleScore, satisfyCriterion.value)) { + sum += aggregateData.counts[possibleScore]; + } + } + return sum; + } + + private isPercentThresholdSatisfied( + satisfyCriterion: any, + aggregateData: any, + sum: number + ): boolean { + const percentOfScores = (100 * sum) / aggregateData.scoreCount; + return percentOfScores >= satisfyCriterion.percentThreshold; + } + + private greaterThanEqualTo(a: number, b: number): boolean { + return a >= b; + } + + private greaterThan(a: number, b: number): boolean { + return a > b; + } + + private lessThanEqualTo(a: number, b: number): boolean { + return a <= b; + } + + private lessThan(a: number, b: number): boolean { + return a < b; + } + + private equalTo(a: number, b: number): boolean { + return a === b; + } + + private notEqualTo(a: number, b: number): boolean { + return a !== b; + } +} diff --git a/src/assets/wise5/services/milestoneService.ts b/src/assets/wise5/services/milestoneService.ts index bc0e6114f73..9b0acecb883 100644 --- a/src/assets/wise5/services/milestoneService.ts +++ b/src/assets/wise5/services/milestoneService.ts @@ -7,60 +7,15 @@ import { ProjectService } from './projectService'; import { TeacherDataService } from './teacherDataService'; import { Injectable } from '@angular/core'; import { copy } from '../common/object/object'; +import { MilestoneCriteriaEvaluator } from '../classroomMonitor/milestones/milestoneCriteriaEvaluator'; @Injectable() export class MilestoneService { - numberOfStudentsCompletedStorage: any[] = []; numberOfStudentsInRun: number; - percentageCompletedStorage: any[] = []; periodId: any; projectMilestones: any[]; + private milestoneCriteriaEvaluator = new MilestoneCriteriaEvaluator(); workgroupIds: any[]; - workgroupsStorage: any[] = []; - satisfyCriteriaFuncNameToFunc = { - percentOfScoresGreaterThan: (satisfyCriterion: any, aggregateAutoScores: any[]) => { - return this.isPercentOfScoresSatisfiesComparator( - satisfyCriterion, - aggregateAutoScores, - this.greaterThan - ); - }, - percentOfScoresGreaterThanOrEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { - return this.isPercentOfScoresSatisfiesComparator( - satisfyCriterion, - aggregateAutoScores, - this.greaterThanEqualTo - ); - }, - percentOfScoresLessThan: (satisfyCriterion: any, aggregateAutoScores: any[]) => { - return this.isPercentOfScoresSatisfiesComparator( - satisfyCriterion, - aggregateAutoScores, - this.lessThan - ); - }, - percentOfScoresLessThanOrEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { - return this.isPercentOfScoresSatisfiesComparator( - satisfyCriterion, - aggregateAutoScores, - this.lessThanEqualTo - ); - }, - percentOfScoresEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { - return this.isPercentOfScoresSatisfiesComparator( - satisfyCriterion, - aggregateAutoScores, - this.equalTo - ); - }, - percentOfScoresNotEqualTo: (satisfyCriterion: any, aggregateAutoScores: any[]) => { - return this.isPercentOfScoresSatisfiesComparator( - satisfyCriterion, - aggregateAutoScores, - this.notEqualTo - ); - } - }; constructor( private achievementService: AchievementService, @@ -70,30 +25,6 @@ export class MilestoneService { private teacherDataService: TeacherDataService ) {} - private greaterThanEqualTo(a: number, b: number): boolean { - return a >= b; - } - - private greaterThan(a: number, b: number): boolean { - return a > b; - } - - private lessThanEqualTo(a: number, b: number): boolean { - return a <= b; - } - - private lessThan(a: number, b: number): boolean { - return a < b; - } - - private equalTo(a: number, b: number): boolean { - return a === b; - } - - private notEqualTo(a: number, b: number): boolean { - return a !== b; - } - getProjectMilestones() { const achievements = this.projectService.getAchievements(); if (achievements.isEnabled) { @@ -104,15 +35,8 @@ export class MilestoneService { return []; } - getProjectMilestoneReports() { - return this.getProjectMilestones().filter((milestone) => { - return milestone.type === 'milestoneReport'; - }); - } - - getMilestoneReportByNodeId(nodeId: string) { - const milestoneReports = this.getProjectMilestoneReports(); - for (const milestonReport of milestoneReports) { + getMilestoneReportByNodeId(nodeId: string): any { + for (const milestonReport of this.getProjectMilestoneReports()) { const referencedComponent = this.getReferencedComponent(milestonReport); if (referencedComponent.nodeId === nodeId) { return this.getProjectMilestoneStatus(milestonReport.id); @@ -121,6 +45,12 @@ export class MilestoneService { return null; } + private getProjectMilestoneReports(): any[] { + return this.getProjectMilestones().filter((milestone) => { + return milestone.type === 'milestoneReport'; + }); + } + getProjectMilestoneStatus(milestoneId: string) { this.periodId = this.teacherDataService.getCurrentPeriod().periodId; this.setWorkgroupsInCurrentPeriod(); @@ -298,10 +228,10 @@ export class MilestoneService { }; } - isTemplateMatch(template: any, aggregateAutoScores: any[]) { + isTemplateMatch(template: any, aggregateAutoScores: any[]): boolean { const matchedCriteria = []; for (const satisfyCriterion of template.satisfyCriteria) { - if (this.isTemplateCriterionSatisfied(satisfyCriterion, aggregateAutoScores)) { + if (this.milestoneCriteriaEvaluator.isSatisfied(satisfyCriterion, aggregateAutoScores)) { matchedCriteria.push(satisfyCriterion); } } @@ -312,64 +242,6 @@ export class MilestoneService { } } - isTemplateCriterionSatisfied(satisfyCriterion: any, aggregateAutoScores: any[]) { - if (satisfyCriterion.function === 'default') { - return true; - } - return this.satisfyCriteriaFuncNameToFunc[satisfyCriterion.function]( - satisfyCriterion, - aggregateAutoScores - ); - } - - private getComparatorSum( - satisfyCriterion: any, - aggregateData: any, - possibleScores: number[], - comparator: any - ): number { - let sum = 0; - for (const possibleScore of possibleScores) { - if (comparator(possibleScore, satisfyCriterion.value)) { - sum += aggregateData.counts[possibleScore]; - } - } - return sum; - } - - private isPercentOfScoresSatisfiesComparator( - satisfyCriterion: any, - aggregateAutoScores: any[], - comparator: any - ): boolean { - const aggregateData = this.getAggregateData(satisfyCriterion, aggregateAutoScores); - const possibleScores = this.getPossibleScores(aggregateData); - const sum = this.getComparatorSum(satisfyCriterion, aggregateData, possibleScores, comparator); - return this.isPercentThresholdSatisfied(satisfyCriterion, aggregateData, sum); - } - - getAggregateData(satisfyCriterion: any, aggregateAutoScores: any[]) { - for (const aggregateAutoScore of aggregateAutoScores) { - if (aggregateAutoScore.componentId === satisfyCriterion.componentId) { - return aggregateAutoScore.aggregateAutoScore[satisfyCriterion.targetVariable]; - } - } - throw new Error(`Aggregate data not found for component ${satisfyCriterion.componentId}`); - } - - getPossibleScores(aggregateData: any) { - return Object.keys(aggregateData.counts).map(Number).sort(); - } - - private isPercentThresholdSatisfied( - satisfyCriterion: any, - aggregateData: any, - sum: number - ): boolean { - const percentOfScores = (100 * sum) / aggregateData.scoreCount; - return percentOfScores >= satisfyCriterion.percentThreshold; - } - getSatisfyCriteriaReferencedComponents(projectAchievement: any) { const components = {}; const templates = projectAchievement.report.templates;