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 aef147312714e8046b1476e4c71beb58bbb4eaa6..51a63294edee269d369d1cd96b5a370d5c5861f0 100644 --- a/src/frontend/src/app/control-center/control-center.component.html +++ b/src/frontend/src/app/control-center/control-center.component.html @@ -6,6 +6,51 @@ <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> @@ -104,38 +149,4 @@ <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> - </div> - </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 142d22d38622e3698177b13c6d936b2539c2c2d7..db9fdc155549968aa829e999e9ac5d6072998fe4 100644 --- a/src/frontend/src/app/control-center/control-center.component.ts +++ b/src/frontend/src/app/control-center/control-center.component.ts @@ -1,7 +1,7 @@ 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 } from '../services/api.service' +import { ApiService, AuthGroup, AuthPermission, AuthUser, Statistics, ApplicationTokens, AnalyticsToken } from '../services/api.service' import { Chart, registerables } from 'chart.js'; import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; @@ -33,6 +33,14 @@ export class ControlCenterComponent { protected responseMessage: string | null = null; protected responseType: 'success' | 'error' = 'success'; + + protected selectedAnalysis?: AnalyticsToken; + protected analyticsToken: AnalyticsToken[] = []; + protected responseStatementMessage: string | null = null; + protected responseStatementType: 'success' | 'error' = 'success'; + + protected filterObject: string = ''; + expandSet = new Set<number>(); protected user_group_checkboxes: UserGroupCheckboxMap = {} protected user_permission_checkboxes: UserGroupCheckboxMap = {} @@ -98,6 +106,10 @@ export class ControlCenterComponent { this._apiService.getApplicationTokens().subscribe(provider => { this.providers = provider; }) + + this._apiService.getAnalyticsTokens().subscribe(provider => { + this.analyticsToken = provider; + }) } renderChart() { @@ -154,7 +166,7 @@ export class ControlCenterComponent { sendStatement() { console.log('Sending xAPI Statement:', this.xapiStatement, 'to', this.selectedProvider); this.responseMessage = `Error:`; - this._apiService.sendStatement(Object.values(JSON.parse(this.xapiStatement)),this.selectedProvider?.keys[0]??'') + this._apiService.sendStatement(JSON.parse(this.xapiStatement),this.selectedProvider?.keys[0]??'') .pipe( catchError(err => { this.responseMessage = `Error: ${JSON.stringify(err.error, null, 2)}`; @@ -193,4 +205,23 @@ export class ControlCenterComponent { ], null, 2); } + + sendFilter() { + console.log('Sending xAPI Filter:', this.filterObject, 'to', this.selectedAnalysis); + + this._apiService.getStatement(this.filterObject??JSON.parse(this.filterObject),this.selectedAnalysis?.key??'', 10) + .pipe( + catchError(err => { + this.responseStatementMessage = `Error: ${JSON.stringify(err.error, null, 2)}`; + this.responseStatementType = 'error'; + return of(null); + }) + ) + .subscribe(result => { + if(result) { + this.responseStatementMessage = JSON.stringify(result, null, 2); + this.responseStatementType = 'success'; + } +}); + } } diff --git a/src/frontend/src/app/interceptors/jwt.interceptor.ts b/src/frontend/src/app/interceptors/jwt.interceptor.ts index 2509f3585fdedab0812106fc29626f761e66269e..95ea05d6b100752ac8b68778b13b88d45a1b20d0 100644 --- a/src/frontend/src/app/interceptors/jwt.interceptor.ts +++ b/src/frontend/src/app/interceptors/jwt.interceptor.ts @@ -10,7 +10,7 @@ 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")) { + if (accessToken && !request.url.includes("/xapi/statements") && !request.url.includes("/api/v1/provider/data")) { 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 c6aa5aa6d95236ab536a991d9250591870dee8b8..d14ba251f70b387e4d92b6a10062c191485d7cb8 100644 --- a/src/frontend/src/app/services/api.service.ts +++ b/src/frontend/src/app/services/api.service.ts @@ -218,6 +218,17 @@ export class ApiService { ], { headers }) } + getStatement(filter: any, key: String, page_size: number): 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/data`, { + ...filter + }, { headers }) + } + getUserConsent(providerId: number): Observable<UserConsentResponse> { return this.http.get<UserConsentResponse>( `${environment.apiUrl}/api/v1/consents/user/${providerId}` diff --git a/src/providers/serializers.py b/src/providers/serializers.py index 852d1cfce0a4044ce341dc0a7a398f6f15de2e94..18a5270b58ee771d1c1176b7fa6d586cbebd1a23 100644 --- a/src/providers/serializers.py +++ b/src/providers/serializers.py @@ -127,7 +127,7 @@ class ProviderDataSerializer(serializers.Serializer): class GetProviderDataRequestSerializer(serializers.Serializer): last_object_id = serializers.CharField(required=False) - page_size = serializers.IntegerField() + page_size = serializers.IntegerField(required=False) class AnalyticsEngineAccessSerializer(serializers.Serializer): diff --git a/src/providers/views.py b/src/providers/views.py index f02f69479b9c08fcbb9a9d9442381bb9e880701a..f8da8a3873a03a30dc7f2ba5863672470ce20860 100644 --- a/src/providers/views.py +++ b/src/providers/views.py @@ -36,6 +36,7 @@ from xapi.views import shib_connector_resolver from users.models import CustomUser from .models import (AnalyticsToken, AnalyticsTokenVerb, Provider, ProviderAuthorization, ProviderSchema) +from pymongo import MongoClient def render_provider(provider): @@ -351,7 +352,7 @@ def get_system_statement_query(providers=[]): if len(providers) > 0: provider_filters = [] for provider in providers: - provider_filters.append("system:" + str(provider.id)) + provider_filters.append("system:" + str(provider)) query["$and"].append({"actor.mbox": {"$in": provider_filters}}) else: query["$and"].append({"actor.mbox": {"$regex": "^system"}}) @@ -423,7 +424,7 @@ class GetProviderData(APIView): "$or": [ {"$and": [ {"actor.mbox": {"$exists": True}}, - {"actor.mbox": {"$regex": "^" + settings.ANONYMIZATION_HASH_PREFIX}}, + {"actor.mbox": {"$regex": f"^{settings.ANONYMIZATION_HASH_PREFIX}"}}, {"verb.id": {"$in": anon_verbs}}, ]}, {"actor.mbox": {"$exists": False}}, @@ -459,9 +460,9 @@ class GetProviderData(APIView): return Response({"message": "Token has expired."}, status=status.HTTP_401_UNAUTHORIZED) # Extract filters from request - filters = request.data.copy() # Use request.POST if it's form-data + filters = request.data.copy() last_object_id = filters.pop("last_object_id", None) - page_size = int(filters.pop("page_size", settings.DEFAULT_PAGE_SIZE)) # Default if not provided + page_size = int(filters.pop("page_size", settings.DEFAULT_PAGE_SIZE)) # Fetch Active Verbs db_fetch_start = time.time() @@ -477,7 +478,8 @@ class GetProviderData(APIView): .values_list("provider", flat=True) .distinct() ) - + + collection = lrs_db["statements"] # Fetch Anonymous Verbs anon_verbs = [] for provider in providers: @@ -485,25 +487,35 @@ class GetProviderData(APIView): latest_schema = ProviderSchema.objects.get(provider=provider, superseded_by__isnull=True) except ObjectDoesNotExist: print(f"No consent provider schema found for provider: {provider}") - return JsonResponse({"message": "No consent provider schema found.", "provider": provider}, - safe=False, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - for verb in (verb for group in latest_schema.groups for verb in group["verbs"]): - if verb["id"] in active_verbs and verb.get("allowAnonymizedCollection", False): - min_count = verb.get("allowAnonymizedCollectionMinCount", settings.ANONYMIZATION_DEFAULT_MINIMUM_COUNT) - query_start = time.time() - current_count = len( - collection.distinct("actor.mbox", { - "$and": [ - {"verb.id": verb["id"]}, - {"actor.mbox": {"$exists": True}}, - {"actor.mbox": {"$regex": "^" + settings.ANONYMIZATION_HASH_PREFIX}} - ] - }) - ) - print(f"Query for verb {verb['id']} executed in {time.time() - query_start:.6f} seconds") - if current_count >= min_count: - anon_verbs.append(verb["id"]) + return JsonResponse( + {"message": "No consent provider schema found.", "provider": str(provider)}, + safe=False, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + groups = latest_schema.groups() + print(groups) + + # Iterate properly over the verbs in groups + for group in groups: + for verb in group.verbs.all(): # ManyToManyField usage + if verb.verb_id in active_verbs and verb.allow_anonymized_collection: + min_count = getattr(verb, "allow_anonymized_collection_min_count", settings.ANONYMIZATION_DEFAULT_MINIMUM_COUNT) + + query_start = time.time() + current_count = len( + collection.distinct("actor.mbox", { + "$and": [ + {"verb.id": verb.verb_id}, + {"actor.mbox": {"$exists": True}}, + {"actor.mbox": {"$regex": f"^{settings.ANONYMIZATION_HASH_PREFIX}"}} + ] + }) + ) + print(f"Query for verb {verb.verb_id} executed in {time.time() - query_start:.6f} seconds") + + if current_count >= min_count: + anon_verbs.append(verb.verb_id) print(f"Anonymous verbs determined: {anon_verbs}") # Construct Query with Dynamic Filtering @@ -544,7 +556,6 @@ class GetProviderData(APIView): return Response({"message": "Invalid analytics token " + token}, status=status.HTTP_401_UNAUTHORIZED) - class StoreAnalyticsEngineResult(APIView): """ Endpoint that allow an analytics engine to store results data in the database.