From 8f5e6c5bc2aa2bb8d36b26d1f1b4aedd358bb498 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Wed, 7 May 2025 13:45:49 -0700 Subject: [PATCH 1/8] Send api ping requests on CRater activities --- src/app/services/pingEndpointService.spec.ts | 19 ++++ .../student-teacher-common-services.module.ts | 96 ++++++++++--------- .../component/component.component.ts | 28 ++++-- .../wise5/services/pingEndpointService.ts | 44 +++++++++ 4 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 src/app/services/pingEndpointService.spec.ts create mode 100644 src/assets/wise5/services/pingEndpointService.ts diff --git a/src/app/services/pingEndpointService.spec.ts b/src/app/services/pingEndpointService.spec.ts new file mode 100644 index 00000000000..c28bc555871 --- /dev/null +++ b/src/app/services/pingEndpointService.spec.ts @@ -0,0 +1,19 @@ +import { PingEndpointService } from '../../assets/wise5/services/pingEndpointService'; +import { TestBed, inject } from '@angular/core/testing'; + +let pingEndpointService: PingEndpointService; +describe('PingEndpointService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [PingEndpointService] + }); + pingEndpointService = TestBed.inject(PingEndpointService); + }); + + it('should send ping to endpoint when startPinging() is called', () => {}); + + it('should wait 5 minutes before sending another ping', () => {}); + + it('should stop trying to ping when stopPinging()', () => {}); +}); diff --git a/src/app/student-teacher-common-services.module.ts b/src/app/student-teacher-common-services.module.ts index 158f6dff01f..c27f27ac261 100644 --- a/src/app/student-teacher-common-services.module.ts +++ b/src/app/student-teacher-common-services.module.ts @@ -1,64 +1,65 @@ -import { NgModule } from '@angular/core'; +import { AchievementService } from '../assets/wise5/services/achievementService'; +import { AiChatService } from '../assets/wise5/components/aiChat/aiChatService'; +import { AnimationService } from '../assets/wise5/components/animation/animationService'; +import { AnnotationService } from '../assets/wise5/services/annotationService'; +import { AudioOscillatorService } from '../assets/wise5/components/audioOscillator/audioOscillatorService'; +import { AudioRecorderService } from '../assets/wise5/services/audioRecorderService'; +import { BranchService } from '../assets/wise5/services/branchService'; +import { ClassroomStatusService } from '../assets/wise5/services/classroomStatusService'; +import { ClickToSnipImageService } from '../assets/wise5/services/clickToSnipImageService'; +import { CompletionService } from '../assets/wise5/services/completionService'; +import { ComponentService } from '../assets/wise5/components/componentService'; +import { ComponentServiceLookupService } from '../assets/wise5/services/componentServiceLookupService'; +import { ComponentTypeService } from '../assets/wise5/services/componentTypeService'; +import { ComputerAvatarService } from '../assets/wise5/services/computerAvatarService'; +import { ConceptMapService } from '../assets/wise5/components/conceptMap/conceptMapService'; import { ConfigService } from '../assets/wise5/services/configService'; -import { ProjectService } from '../assets/wise5/services/projectService'; -import { ProjectLibraryService } from '../assets/wise5/services/projectLibraryService'; -import { VLEProjectService } from '../assets/wise5/vle/vleProjectService'; +import { ConstraintService } from '../assets/wise5/services/constraintService'; import { CRaterService } from '../assets/wise5/services/cRaterService'; -import { SessionService } from '../assets/wise5/services/sessionService'; -import { StudentAssetService } from '../assets/wise5/services/studentAssetService'; -import { TagService } from '../assets/wise5/services/tagService'; -import { AudioRecorderService } from '../assets/wise5/services/audioRecorderService'; -import { AnnotationService } from '../assets/wise5/services/annotationService'; -import { StudentWebSocketService } from '../assets/wise5/services/studentWebSocketService'; -import { StudentDataService } from '../assets/wise5/services/studentDataService'; -import { AchievementService } from '../assets/wise5/services/achievementService'; -import { SummaryService } from '../assets/wise5/components/summary/summaryService'; -import { TableService } from '../assets/wise5/components/table/tableService'; -import { NotebookService } from '../assets/wise5/services/notebookService'; -import { NotificationService } from '../assets/wise5/services/notificationService'; -import { OutsideURLService } from '../assets/wise5/components/outsideURL/outsideURLService'; -import { MatchService } from '../assets/wise5/components/match/matchService'; -import { MultipleChoiceService } from '../assets/wise5/components/multipleChoice/multipleChoiceService'; -import { OpenResponseService } from '../assets/wise5/components/openResponse/openResponseService'; -import { NodeService } from '../assets/wise5/services/nodeService'; +import { DialogGuidanceService } from '../assets/wise5/components/dialogGuidance/dialogGuidanceService'; import { DiscussionService } from '../assets/wise5/components/discussion/discussionService'; import { DrawService } from '../assets/wise5/components/draw/drawService'; import { EmbeddedService } from '../assets/wise5/components/embedded/embeddedService'; +import { GraphService } from '../assets/wise5/components/graph/graphService'; import { HTMLService } from '../assets/wise5/components/html/htmlService'; import { LabelService } from '../assets/wise5/components/label/labelService'; -import { AnimationService } from '../assets/wise5/components/animation/animationService'; -import { AudioOscillatorService } from '../assets/wise5/components/audioOscillator/audioOscillatorService'; -import { ConceptMapService } from '../assets/wise5/components/conceptMap/conceptMapService'; -import { GraphService } from '../assets/wise5/components/graph/graphService'; -import { ComponentService } from '../assets/wise5/components/componentService'; -import { WiseLinkService } from './services/wiseLinkService'; -import { DialogGuidanceService } from '../assets/wise5/components/dialogGuidance/dialogGuidanceService'; -import { PeerChatService } from '../assets/wise5/components/peerChat/peerChatService'; -import { ShowMyWorkService } from '../assets/wise5/components/showMyWork/showMyWorkService'; -import { ShowGroupWorkService } from '../assets/wise5/components/showGroupWork/showGroupWorkService'; -import { ComputerAvatarService } from '../assets/wise5/services/computerAvatarService'; -import { StudentStatusService } from '../assets/wise5/services/studentStatusService'; +import { MatchService } from '../assets/wise5/components/match/matchService'; +import { MultipleChoiceService } from '../assets/wise5/components/multipleChoice/multipleChoiceService'; +import { NgModule } from '@angular/core'; +import { NodeProgressService } from '../assets/wise5/services/nodeProgressService'; +import { NodeService } from '../assets/wise5/services/nodeService'; +import { NodeStatusService } from '../assets/wise5/services/nodeStatusService'; +import { NotebookService } from '../assets/wise5/services/notebookService'; +import { NotificationService } from '../assets/wise5/services/notificationService'; import { OpenResponseCompletionCriteriaService } from '../assets/wise5/components/openResponse/openResponseCompletionCriteriaService'; -import { ComponentServiceLookupService } from '../assets/wise5/services/componentServiceLookupService'; -import { ComponentTypeService } from '../assets/wise5/services/componentTypeService'; -import { BranchService } from '../assets/wise5/services/branchService'; +import { OpenResponseService } from '../assets/wise5/components/openResponse/openResponseService'; +import { OutsideURLService } from '../assets/wise5/components/outsideURL/outsideURLService'; import { PathService } from '../assets/wise5/services/pathService'; -import { TabulatorDataService } from '../assets/wise5/components/table/tabulatorDataService'; -import { StompService } from '../assets/wise5/services/stompService'; -import { ClickToSnipImageService } from '../assets/wise5/services/clickToSnipImageService'; -import { StudentPeerGroupService } from '../assets/wise5/services/studentPeerGroupService'; -import { ConstraintService } from '../assets/wise5/services/constraintService'; -import { NodeStatusService } from '../assets/wise5/services/nodeStatusService'; +import { PeerChatService } from '../assets/wise5/components/peerChat/peerChatService'; import { PeerGroupService } from '../assets/wise5/services/peerGroupService'; -import { NodeProgressService } from '../assets/wise5/services/nodeProgressService'; -import { CompletionService } from '../assets/wise5/services/completionService'; +import { PingEndpointService } from '../assets/wise5/services/pingEndpointService'; +import { ProjectLibraryService } from '../assets/wise5/services/projectLibraryService'; +import { ProjectService } from '../assets/wise5/services/projectService'; +import { SessionService } from '../assets/wise5/services/sessionService'; +import { ShowGroupWorkService } from '../assets/wise5/components/showGroupWork/showGroupWorkService'; +import { ShowMyWorkService } from '../assets/wise5/components/showMyWork/showMyWorkService'; +import { StompService } from '../assets/wise5/services/stompService'; +import { StudentAssetService } from '../assets/wise5/services/studentAssetService'; +import { StudentDataService } from '../assets/wise5/services/studentDataService'; import { StudentNodeService } from '../assets/wise5/services/studentNodeService'; +import { StudentPeerGroupService } from '../assets/wise5/services/studentPeerGroupService'; import { StudentProjectTranslationService } from '../assets/wise5/services/studentProjectTranslationService'; -import { AiChatService } from '../assets/wise5/components/aiChat/aiChatService'; -import { TeacherNodeService } from '../assets/wise5/services/teacherNodeService'; +import { StudentStatusService } from '../assets/wise5/services/studentStatusService'; +import { StudentWebSocketService } from '../assets/wise5/services/studentWebSocketService'; +import { SummaryService } from '../assets/wise5/components/summary/summaryService'; +import { TableService } from '../assets/wise5/components/table/tableService'; +import { TabulatorDataService } from '../assets/wise5/components/table/tabulatorDataService'; +import { TagService } from '../assets/wise5/services/tagService'; import { TeacherDataService } from '../assets/wise5/services/teacherDataService'; +import { TeacherNodeService } from '../assets/wise5/services/teacherNodeService'; import { TeacherWebSocketService } from '../assets/wise5/services/teacherWebSocketService'; -import { ClassroomStatusService } from '../assets/wise5/services/classroomStatusService'; +import { VLEProjectService } from '../assets/wise5/vle/vleProjectService'; +import { WiseLinkService } from './services/wiseLinkService'; @NgModule({ providers: [ @@ -100,6 +101,7 @@ import { ClassroomStatusService } from '../assets/wise5/services/classroomStatus PathService, PeerChatService, PeerGroupService, + PingEndpointService, ProjectLibraryService, { provide: ProjectService, useExisting: VLEProjectService }, SessionService, diff --git a/src/assets/wise5/components/component/component.component.ts b/src/assets/wise5/components/component/component.component.ts index 5a5ecbe191a..635dd9f75b4 100644 --- a/src/assets/wise5/components/component/component.component.ts +++ b/src/assets/wise5/components/component/component.component.ts @@ -11,20 +11,21 @@ import { createComponent } from '@angular/core'; import { ClickToSnipImageService } from '../../services/clickToSnipImageService'; -import { ConfigService } from '../../services/configService'; -import { NotebookService } from '../../services/notebookService'; -import { ProjectService } from '../../services/projectService'; -import { StudentDataService } from '../../services/studentDataService'; +import { CommonModule } from '@angular/common'; import { Component as WISEComponent } from '../../common/Component'; import { ComponentFactory } from '../../common/ComponentFactory'; import { components } from '../Components'; +import { ConfigService } from '../../services/configService'; import { HelpIconComponent } from '../../themes/default/themeComponents/helpIcon/help-icon.component'; -import { CommonModule } from '@angular/common'; +import { NotebookService } from '../../services/notebookService'; +import { PingEndpointService } from '../../services/pingEndpointService'; +import { ProjectService } from '../../services/projectService'; +import { StudentDataService } from '../../services/studentDataService'; @Component({ - imports: [CommonModule, HelpIconComponent], - selector: 'component', - templateUrl: 'component.component.html' + imports: [CommonModule, HelpIconComponent], + selector: 'component', + templateUrl: 'component.component.html' }) export class ComponentComponent { protected component: WISEComponent; @@ -32,6 +33,7 @@ export class ComponentComponent { @Input() private componentId: string; private componentRef: ComponentRef; @Input() protected componentState: any; + private componentType: string; @Input() private nodeId: string; protected rubric: string; @Output() protected saveComponentStateEvent: EventEmitter = new EventEmitter(); @@ -45,6 +47,7 @@ export class ComponentComponent { private dataService: StudentDataService, private injector: EnvironmentInjector, private notebookService: NotebookService, + private pingEndpointService: PingEndpointService, private projectService: ProjectService ) {} @@ -63,6 +66,13 @@ export class ComponentComponent { this.rubric = this.component.content.rubric; this.showRubric = this.rubric != null && this.rubric != ''; } + this.pingEndpoint(); + } + + private pingEndpoint() { + if (['DialogGuidance', 'OpenResponse'].includes(this.componentType)) { + this.pingEndpointService.startPinging(this.component, this.componentType); + } } private setComponent(): void { @@ -77,6 +87,7 @@ export class ComponentComponent { } const factory = new ComponentFactory(); this.component = factory.getComponent(content, this.nodeId); + this.componentType = content.type; } ngAfterViewInit(): void { @@ -95,6 +106,7 @@ export class ComponentComponent { } ngOnDestroy(): void { + this.pingEndpointService.stopPinging(); this.componentRef.destroy(); } } diff --git a/src/assets/wise5/services/pingEndpointService.ts b/src/assets/wise5/services/pingEndpointService.ts new file mode 100644 index 00000000000..c193ac331ba --- /dev/null +++ b/src/assets/wise5/services/pingEndpointService.ts @@ -0,0 +1,44 @@ +import { Component } from '../common/Component'; +import { DialogGuidanceComponent } from '../components/dialogGuidance/DialogGuidanceComponent'; +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { OpenResponseContent } from '../components/openResponse/OpenResponseContent'; + +@Injectable() +export class PingEndpointService { + private pingUrl = '/api/c-rater/ping-endpoint'; + private interval: NodeJS.Timeout; + private isPinging: boolean; + private itemId: string = ''; + + constructor(private http: HttpClient) {} + + startPinging(component: Component, componentType: string): void { + if (!this.isPinging) { + this.findItemId(component, componentType); + this.sendPing(); + // 295000 ms = 4min 55sec + this.interval = setInterval(() => this.sendPing(), 295000); + this.isPinging = true; + } + } + + stopPinging(): void { + if (this.isPinging) { + clearInterval(this.interval); + this.isPinging = false; + } + } + + private sendPing(): void { + this.http.post(this.pingUrl, { itemId: this.itemId }).subscribe(() => {}); + } + + private findItemId(component: Component, componentType: string): void { + if (componentType === 'DialogGuidance') { + this.itemId = (component as DialogGuidanceComponent).getItemId(); + } else if (componentType === 'OpenResponse') { + this.itemId = (component.content as OpenResponseContent).cRater?.itemId ?? ''; + } + } +} From 60bcb3d39a8f753f6559a9fca06a7d4e05441e44 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 9 May 2025 23:49:52 -0700 Subject: [PATCH 2/8] Write tests --- src/app/services/pingEndpointService.spec.ts | 45 ++++++++++++++++--- .../component/component.component.ts | 2 +- .../wise5/services/pingEndpointService.ts | 3 +- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/app/services/pingEndpointService.spec.ts b/src/app/services/pingEndpointService.spec.ts index c28bc555871..20bcbdd6d60 100644 --- a/src/app/services/pingEndpointService.spec.ts +++ b/src/app/services/pingEndpointService.spec.ts @@ -1,19 +1,54 @@ +import { HttpClient } from '@angular/common/http'; +import { Component } from '../../assets/wise5/common/Component'; import { PingEndpointService } from '../../assets/wise5/services/pingEndpointService'; -import { TestBed, inject } from '@angular/core/testing'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { MockProvider } from 'ng-mocks'; +import { DialogGuidanceComponent } from '../../assets/wise5/components/dialogGuidance/DialogGuidanceComponent'; +import { DialogGuidanceContent } from '../../assets/wise5/components/dialogGuidance/DialogGuidanceContent'; +import { of } from 'rxjs'; let pingEndpointService: PingEndpointService; +let httpClientMock: jasmine.Spy; +let component: Component; + describe('PingEndpointService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [PingEndpointService] + providers: [MockProvider(HttpClient), PingEndpointService] }); pingEndpointService = TestBed.inject(PingEndpointService); + httpClientMock = spyOn(TestBed.inject(HttpClient), 'post').and.returnValue(of({})); + + const dgContent: DialogGuidanceContent = { + computerAvatarSettings: { ids: [], label: 'l', prompt: 'p', initialResponse: 'i' }, + feedbackRules: [], + id: 'id', + isComputerAvatarEnabled: false, + itemId: 'itemId', + type: 'DialogGuidance' + }; + component = new DialogGuidanceComponent(dgContent, 'nodeId'); }); - it('should send ping to endpoint when startPinging() is called', () => {}); + it('should send ping to endpoint when startPinging() is called', () => { + pingEndpointService.startPinging(component); + expect(httpClientMock).toHaveBeenCalled(); + }); - it('should wait 5 minutes before sending another ping', () => {}); + it('should wait before sending another ping', fakeAsync(() => { + pingEndpointService.startPinging(component); + tick(294999); + expect(httpClientMock).toHaveBeenCalledTimes(1); + tick(2); + expect(httpClientMock).toHaveBeenCalledTimes(2); + })); - it('should stop trying to ping when stopPinging()', () => {}); + it('should stop trying to ping when stopPinging()', fakeAsync(() => { + pingEndpointService.startPinging(component); + expect(httpClientMock).toHaveBeenCalledTimes(1); + pingEndpointService.stopPinging(); + tick(500000); + expect(httpClientMock).toHaveBeenCalledTimes(1); + })); }); diff --git a/src/assets/wise5/components/component/component.component.ts b/src/assets/wise5/components/component/component.component.ts index 635dd9f75b4..02d3905fe0e 100644 --- a/src/assets/wise5/components/component/component.component.ts +++ b/src/assets/wise5/components/component/component.component.ts @@ -71,7 +71,7 @@ export class ComponentComponent { private pingEndpoint() { if (['DialogGuidance', 'OpenResponse'].includes(this.componentType)) { - this.pingEndpointService.startPinging(this.component, this.componentType); + this.pingEndpointService.startPinging(this.component); } } diff --git a/src/assets/wise5/services/pingEndpointService.ts b/src/assets/wise5/services/pingEndpointService.ts index c193ac331ba..03c0d046d65 100644 --- a/src/assets/wise5/services/pingEndpointService.ts +++ b/src/assets/wise5/services/pingEndpointService.ts @@ -13,7 +13,8 @@ export class PingEndpointService { constructor(private http: HttpClient) {} - startPinging(component: Component, componentType: string): void { + startPinging(component: Component): void { + const componentType = component.content.type; if (!this.isPinging) { this.findItemId(component, componentType); this.sendPing(); From f7337e7d45c269226de2c3ffe519b197548c25e5 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 12 May 2025 14:41:52 -0700 Subject: [PATCH 3/8] Ping on focus --- src/app/services/pingEndpointService.spec.ts | 38 +++++++++++++---- .../chat-input/chat-input.component.html | 1 + .../chat-input/chat-input.component.spec.ts | 9 ++-- .../common/chat-input/chat-input.component.ts | 19 +++++++-- .../component/component.component.ts | 15 +++++-- .../open-response-student.component.html | 1 + .../open-response-student.component.spec.ts | 40 ++++++++++-------- .../open-response-student.component.ts | 26 ++++++++---- .../open-response-student.module.ts | 3 +- .../wise5/services/pingEndpointService.ts | 41 ++++++++++--------- 10 files changed, 128 insertions(+), 65 deletions(-) diff --git a/src/app/services/pingEndpointService.spec.ts b/src/app/services/pingEndpointService.spec.ts index 20bcbdd6d60..b6c490652cb 100644 --- a/src/app/services/pingEndpointService.spec.ts +++ b/src/app/services/pingEndpointService.spec.ts @@ -31,24 +31,44 @@ describe('PingEndpointService', () => { component = new DialogGuidanceComponent(dgContent, 'nodeId'); }); - it('should send ping to endpoint when startPinging() is called', () => { - pingEndpointService.startPinging(component); - expect(httpClientMock).toHaveBeenCalled(); + it('should add to ping list iff item id is unique and for berkeley endpoint', () => { + expect(getPingListSize()).toEqual(0); + pingEndpointService.addItemToPingList('test'); + expect(getPingListSize()).toEqual(0); + pingEndpointService.addItemToPingList('berkeley_test'); + expect(getPingListSize()).toEqual(1); + pingEndpointService.addItemToPingList('berkeley_test'); + expect(getPingListSize()).toEqual(1); + }); + + it('should send pings to endpoint when startPinging() is called', () => { + pingEndpointService.addItemToPingList('berkeley_test1'); + pingEndpointService.addItemToPingList('berkeley_test2'); + pingEndpointService.startPinging(); + expect(httpClientMock).toHaveBeenCalledTimes(2); }); it('should wait before sending another ping', fakeAsync(() => { - pingEndpointService.startPinging(component); + pingEndpointService.addItemToPingList('berkeley_test1'); + pingEndpointService.addItemToPingList('berkeley_test2'); + pingEndpointService.startPinging(); tick(294999); - expect(httpClientMock).toHaveBeenCalledTimes(1); - tick(2); expect(httpClientMock).toHaveBeenCalledTimes(2); + tick(2); + expect(httpClientMock).toHaveBeenCalledTimes(4); })); it('should stop trying to ping when stopPinging()', fakeAsync(() => { - pingEndpointService.startPinging(component); - expect(httpClientMock).toHaveBeenCalledTimes(1); + pingEndpointService.addItemToPingList('berkeley_test1'); + pingEndpointService.addItemToPingList('berkeley_test2'); + pingEndpointService.startPinging(); + expect(httpClientMock).toHaveBeenCalledTimes(2); pingEndpointService.stopPinging(); tick(500000); - expect(httpClientMock).toHaveBeenCalledTimes(1); + expect(httpClientMock).toHaveBeenCalledTimes(2); })); }); + +function getPingListSize(): number { + return [...pingEndpointService['pingList']].length; +} diff --git a/src/assets/wise5/common/chat-input/chat-input.component.html b/src/assets/wise5/common/chat-input/chat-input.component.html index d284b34aee1..167164ab477 100644 --- a/src/assets/wise5/common/chat-input/chat-input.component.html +++ b/src/assets/wise5/common/chat-input/chat-input.component.html @@ -7,6 +7,7 @@ i18n-placeholder [(ngModel)]="response" (keypress)="keyPressed($event)" + (focus)="onFocus()" >