diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts index 2c459d598ffcb10f129d0c8747164d943e209964..1a6af97a01f7afcec8b3ee1e6ec54f628cb27e69 100644 --- a/src/frontend/src/app/app.module.ts +++ b/src/frontend/src/app/app.module.ts @@ -81,6 +81,7 @@ import { CreateVerbGroupDialog } from './dialogs/create-verb-group-dialog/create import { NzRadioModule } from 'ng-zorro-antd/radio' import { PrivacyPolicyDialog } from './dialogs/privacy-policy-dialog/privacy-policy-dialog' import { MarkdownEditorComponent } from './helper-components/markdown-editor/markdown-editor.component' +import { ResultToolsComponent } from './control-center/result-tools/result-tools.component' registerLocaleData(de) @@ -115,7 +116,8 @@ registerLocaleData(de) AnalyticsEngineComponent, VerbGroupsComponent, PrivacyPolicyDialog, - MarkdownEditorComponent + MarkdownEditorComponent, + ResultToolsComponent, ], imports: [ BrowserModule, diff --git a/src/frontend/src/app/control-center/control-center.component.html b/src/frontend/src/app/control-center/control-center.component.html index 51a63294edee269d369d1cd96b5a370d5c5861f0..a5e1e29171dd7139d03a1bbba50b1322be83f657 100644 --- a/src/frontend/src/app/control-center/control-center.component.html +++ b/src/frontend/src/app/control-center/control-center.component.html @@ -6,52 +6,6 @@ <nz-tabset> - <nz-tab [nzTitle]="tabTitlexAPI"> - <ng-template i18n="xAPI Tools @@xAPITools" #tabTitlexAPI> - xAPI Tools - </ng-template> - <div class="xapi-tools-container"> - <h3 i18n="@@controlCenterxAPIStatementDebugger"> - xAPI Statement Debugger - </h3> - <p i18n="@@controlCenterDescription"> - Select a provider from the list and enter a statement to view the result of the rights engine. - </p> - <nz-select [(ngModel)]="selectedProvider" placeholder="Select Provider"> - <nz-option *ngFor="let provider of this.providers" [nzValue]="provider" [nzLabel]="provider.provider.name"></nz-option> - </nz-select> - <textarea rows="20" [(ngModel)]="xapiStatement" placeholder="Enter xAPI statement"></textarea> - <nz-space> - <button *nzSpaceItem nz-button nzType="primary" (click)="sendStatement()">Send Statement</button> - <button *nzSpaceItem nz-button nzType="default" (click)="generateStatement()">Generate Statement</button> - </nz-space> - <div class="response-container" *ngIf="this.responseMessage"> - - <nz-alert [nzType]="this.responseType" [nzMessage]="this.responseMessage" nzShowIcon></nz-alert> - </div> - </div> - - <div class="xapi-tools-container"> - <h3 i18n="@@controlCenterxAPIStatementDebugger"> - xAPI Statement Filter Debugger - </h3> - <p i18n="@@controlCenterDescription"> - Select an analytics from the list to execute the statement fetch that can be done by the analytics - </p> - <nz-select [(ngModel)]="selectedAnalysis" placeholder="Select Analytics"> - <nz-option *ngFor="let provider of this.analyticsToken" [nzValue]="provider" [nzLabel]="provider.name"></nz-option> - </nz-select> - <textarea rows="10" [(ngModel)]="filterObject" placeholder="Filter Query"></textarea> - <nz-space> - <button *nzSpaceItem nz-button nzType="primary" (click)="sendFilter()">Get statements</button> - </nz-space> - <div class="response-container" *ngIf="this.responseStatementMessage"> - - <nz-alert [nzType]="this.responseStatementType" [nzMessage]="this.responseStatementMessage" nzShowIcon></nz-alert> - </div> - </div> - </nz-tab> - <nz-tab [nzTitle]="tabTitleOverview"> <ng-template i18n="Overview @@Overview" #tabTitleOverview> Übersicht @@ -149,4 +103,57 @@ <button nz-button style="float: right" (click)="savePrivacyPolicy()">Datenschutzerklärung speichern</button> </nz-tab> + <nz-tab [nzTitle]="tabTitlexAPI"> + <ng-template i18n="xAPI Tools @@xAPITools" #tabTitlexAPI> + xAPI Tools + </ng-template> + <div class="xapi-tools-container"> + <h3 i18n="@@controlCenterxAPIStatementDebugger"> + xAPI Statement Debugger + </h3> + <p i18n="@@controlCenterDescription"> + Select a provider from the list and enter a statement to view the result of the rights engine. + </p> + <nz-select [(ngModel)]="selectedProvider" placeholder="Select Provider"> + <nz-option *ngFor="let provider of this.providers" [nzValue]="provider" [nzLabel]="provider.provider.name"></nz-option> + </nz-select> + <textarea rows="20" [(ngModel)]="xapiStatement" placeholder="Enter xAPI statement"></textarea> + <nz-space> + <button *nzSpaceItem nz-button nzType="primary" (click)="sendStatement()">Send Statement</button> + <button *nzSpaceItem nz-button nzType="default" (click)="generateStatement()">Generate Statement</button> + </nz-space> + <div class="response-container" *ngIf="this.responseMessage"> + + <nz-alert [nzType]="this.responseType" [nzMessage]="this.responseMessage" nzShowIcon></nz-alert> + </div> + </div> + + <div class="xapi-tools-container"> + <h3 i18n="@@controlCenterxAPIStatementDebugger"> + xAPI Statement Filter Debugger + </h3> + <p i18n="@@controlCenterDescription"> + Select an analytics from the list to execute the statement fetch that can be done by the analytics + </p> + <nz-select [(ngModel)]="selectedAnalysis" placeholder="Select Analytics"> + <nz-option *ngFor="let provider of this.analyticsToken" [nzValue]="provider" [nzLabel]="provider.name"></nz-option> + </nz-select> + <textarea rows="10" [(ngModel)]="filterObject" placeholder="Filter Query"></textarea> + <nz-space> + <button *nzSpaceItem nz-button nzType="primary" (click)="sendFilter()">Get statements</button> + </nz-space> + <div class="response-container" *ngIf="this.responseStatementMessage"> + + <nz-alert [nzType]="this.responseStatementType" [nzMessage]="this.responseStatementMessage" nzShowIcon></nz-alert> + </div> + </div> + </nz-tab> + + <nz-tab [nzTitle]="tabTitleResultTools"> + <ng-template i18n="Analytics Result Tools @@ResultTools" #tabTitleResultTools> + Analytics Result Tools + </ng-template> + <app-result-tools></app-result-tools> + </nz-tab> + </nz-tabset> diff --git a/src/frontend/src/app/control-center/control-center.component.ts b/src/frontend/src/app/control-center/control-center.component.ts index db9fdc155549968aa829e999e9ac5d6072998fe4..090735493643f3f2dc68debb3bad2168f14b81eb 100644 --- a/src/frontend/src/app/control-center/control-center.component.ts +++ b/src/frontend/src/app/control-center/control-center.component.ts @@ -176,7 +176,7 @@ export class ControlCenterComponent { ) .subscribe(result => { if(result) { - this.responseMessage = result; + this.responseMessage = JSON.stringify(result, null, 2); this.responseType = 'success'; } }); diff --git a/src/frontend/src/app/control-center/result-tools/result-tools.component.html b/src/frontend/src/app/control-center/result-tools/result-tools.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9297c78b499314e92a41317d1f325e39da631761 --- /dev/null +++ b/src/frontend/src/app/control-center/result-tools/result-tools.component.html @@ -0,0 +1,20 @@ +<div class="xapi-tools-container"> + <h3 i18n="@@controlCenterResultImport"> + Result Importer + </h3> + <p i18n="@@controlCenterResultImportDescription"> + Enter JSON based results to import to the lrs. Please select an analytics to import the results. + </p> + <nz-select [(ngModel)]="selectedAnalysis" placeholder="Select Analytics"> + <nz-option *ngFor="let provider of this.analyticsToken" [nzValue]="provider" [nzLabel]="provider.name"></nz-option> + </nz-select> + <textarea rows="10" [(ngModel)]="resultObject" placeholder="Result to import"></textarea> + <nz-space> + <button *nzSpaceItem nz-button nzType="primary" (click)="importResults()">Import results</button> + <button *nzSpaceItem nz-button nzType="default" (click)="generateExample()">Example result</button> + </nz-space> + <div class="response-container" *ngIf="this.responseMessage"> + + <nz-alert [nzType]="this.responseType" [nzMessage]="this.responseMessage" nzShowIcon></nz-alert> + </div> +</div> \ No newline at end of file diff --git a/src/frontend/src/app/control-center/result-tools/result-tools.component.scss b/src/frontend/src/app/control-center/result-tools/result-tools.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..ff4e40078367d0d9ceb132fab5a55047b588a369 --- /dev/null +++ b/src/frontend/src/app/control-center/result-tools/result-tools.component.scss @@ -0,0 +1,25 @@ +.control-center { + padding: 20px; + } + .statistic-container { + display: flex; + gap: 20px; + justify-content: center; + } + .statistic-card { + width: 300px; + text-align: center; + background-color: white !important; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; + } + .chart-container { + width: 100%; + height: 200px; + } + .xapi-tools-container { + display: flex; + flex-direction: column; + gap: 10px; + padding: 20px; + } \ No newline at end of file diff --git a/src/frontend/src/app/control-center/result-tools/result-tools.component.spec.ts b/src/frontend/src/app/control-center/result-tools/result-tools.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0252013f8d51fb02c1264f86ed5dc30fcdf72004 --- /dev/null +++ b/src/frontend/src/app/control-center/result-tools/result-tools.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResultToolsComponent } from './result-tools.component'; + +describe('ResultToolsComponent', () => { + let component: ResultToolsComponent; + let fixture: ComponentFixture<ResultToolsComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ResultToolsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ResultToolsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/src/app/control-center/result-tools/result-tools.component.ts b/src/frontend/src/app/control-center/result-tools/result-tools.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..12f2f32a3b06fd460f76af111cf14c2c3fa094c5 --- /dev/null +++ b/src/frontend/src/app/control-center/result-tools/result-tools.component.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core' +import { NzMessageService } from 'ng-zorro-antd/message' +import { NzModalService } from 'ng-zorro-antd/modal' +import { ApiService, AuthGroup, AuthPermission, AuthUser, Statistics, ApplicationTokens, AnalyticsToken } from '../../services/api.service' +import { catchError } from 'rxjs/operators'; +import { of } from 'rxjs'; + +@Component({ + selector: 'app-result-tools', + templateUrl: './result-tools.component.html', + styleUrls: ['./result-tools.component.scss'] +}) +export class ResultToolsComponent { + + protected selectedAnalysis?: AnalyticsToken; + protected analyticsToken: AnalyticsToken[] = []; + protected responseMessage: string | null = null; + protected responseType: 'success' | 'error' = 'success'; + + protected resultObject: string = ''; + + + constructor( + private _messageService: NzMessageService, + private _apiService: ApiService, + public dialog: NzModalService + ) { + + this._apiService.getAnalyticsTokens().subscribe(provider => { + this.analyticsToken = provider; + }) + } + + importResults() { + console.log('Sending xAPI Statement:', this.resultObject, 'to', this.selectedAnalysis); + + let parsedResultObject; + + try { + parsedResultObject = typeof this.resultObject === 'string' + ? JSON.parse(this.resultObject) + : this.resultObject; + } catch (error) { + this.responseMessage = 'Error: Invalid JSON format in resultObject'; + this.responseType = 'error'; + return; + } + + if (!parsedResultObject) { + this.responseMessage = 'Error: resultObject is null or undefined'; + this.responseType = 'error'; + return; + } + + this._apiService.storeResult(parsedResultObject, this.selectedAnalysis?.key ?? '') + .pipe( + catchError(err => { + this.responseMessage = `Error: ${JSON.stringify(err.error, null, 2)}`; + this.responseType = 'error'; + return of(null); + }) + ) + .subscribe(result => { + if (result) { + this.responseMessage = JSON.stringify(result, null, 2); + this.responseType = 'success'; + } + }); + } + + + generateExample() { + this.resultObject = JSON.stringify({ + result: { + score: Math.floor(Math.random() * 100), + completion: Math.random() > 0.5, + success: Math.random() > 0.5, + duration: `${Math.floor(Math.random() * 3600)}s`, + response: "Example response text", + }, + context_id: `context-${Math.random().toString(36).substr(2, 9)}` + }, null,2); + } +} \ No newline at end of file diff --git a/src/frontend/src/app/interceptors/jwt.interceptor.ts b/src/frontend/src/app/interceptors/jwt.interceptor.ts index 95ea05d6b100752ac8b68778b13b88d45a1b20d0..5f3dabddf67e8943a1536ead435a84e8bb1523fa 100644 --- a/src/frontend/src/app/interceptors/jwt.interceptor.ts +++ b/src/frontend/src/app/interceptors/jwt.interceptor.ts @@ -10,7 +10,10 @@ export class JwtInterceptor implements HttpInterceptor { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const accessToken = this._authService.accessToken //const isApiUrl = request.url.startsWith(environment.apiUrl); - if (accessToken && !request.url.includes("/xapi/statements") && !request.url.includes("/api/v1/provider/data")) { + if (accessToken + && !request.url.includes("/xapi/statements") + && !request.url.includes("/api/v1/provider/data") + && !request.url.includes("/api/v1/provider/store-result")) { request = request.clone({ setHeaders: { Authorization: `Bearer ${accessToken}` } }) diff --git a/src/frontend/src/app/services/api.service.ts b/src/frontend/src/app/services/api.service.ts index d14ba251f70b387e4d92b6a10062c191485d7cb8..92fcd60873cd8f6293f6e9e6564a8ecfb477cbec 100644 --- a/src/frontend/src/app/services/api.service.ts +++ b/src/frontend/src/app/services/api.service.ts @@ -225,10 +225,21 @@ export class ApiService { }); return this.http.post<string>(`${environment.apiUrl}/api/v1/provider/data`, { - ...filter + ...filter, + page_size: page_size }, { headers }) } + storeResult(results: any, key: String): Observable<string> { + const headers = new HttpHeaders({ + 'Content-Type': 'application/json', + 'Authorization': `Basic ${key}` // Replace with actual token retrieval method + }); + + return this.http.post<string>(`${environment.apiUrl}/api/v1/provider/store-result`, { + ...results}, { headers }) + } + getUserConsent(providerId: number): Observable<UserConsentResponse> { return this.http.get<UserConsentResponse>( `${environment.apiUrl}/api/v1/consents/user/${providerId}` diff --git a/src/providers/views.py b/src/providers/views.py index f8da8a3873a03a30dc7f2ba5863672470ce20860..f6a629ea3d5b06cb05f436e086aadd8712bed45f 100644 --- a/src/providers/views.py +++ b/src/providers/views.py @@ -558,15 +558,22 @@ class GetProviderData(APIView): class StoreAnalyticsEngineResult(APIView): """ - Endpoint that allow an analytics engine to store results data in the database. + Endpoint that allows an analytics engine to store results data in the database. """ def post(self, request): - token = request.data.get("token") + if token is None: - token = request.headers.get("Authorization").split("Basic ")[1] - + auth_header = request.headers.get("Authorization") + if auth_header and "Basic " in auth_header: + token = auth_header.split("Basic ")[1] + else: + return Response( + {"message": "Missing token in request data and Authorization header."}, + status=status.HTTP_400_BAD_REQUEST, + ) + try: analytics_token = AnalyticsToken.objects.get(key=token) collection = lrs_db["results"] @@ -588,20 +595,19 @@ class StoreAnalyticsEngineResult(APIView): } ) return Response( - {"message": "result stored"}, + {"message": "Result stored successfully."}, status=status.HTTP_200_OK, ) - except ObjectDoesNotExist as c: - print(c) + except ObjectDoesNotExist: return Response( - {"message": "Invalid analytics token: " + token}, + {"message": "Invalid analytics token."}, status=status.HTTP_400_BAD_REQUEST, ) except Exception as e: print(e) return Response( {"message": "Failed to store analytics engine result."}, - status=status.HTTP_400_BAD_REQUEST, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, )