diff --git a/src/README.md b/src/README.md index bea98e4057762c7dd65c708ab00a4479725b794d..f4c7c11a102e7e4f52a9bf3ea29a05b9b82114ac 100644 --- a/src/README.md +++ b/src/README.md @@ -1,7 +1,7 @@ # Polaris Backend ## virtualenv -First, you should create an virtual env, to start the development: +First, you should create a virtual environment to start the development: ```console $ pip3 install virtualenv @@ -147,4 +147,4 @@ $ curl -X POST 127.0.0.1:8003/api/v1/provider/visualization-tokens/create --data ```console $ python manage.py test -``` \ No newline at end of file +``` diff --git a/src/backend/.env.dist b/src/backend/.env.dist index bb0bdd3cb400acbbc9cfc26980148ea476867342..cf392f8b45ceaa7baed330c9aac91c22f62a1a60 100644 --- a/src/backend/.env.dist +++ b/src/backend/.env.dist @@ -15,4 +15,7 @@ LRS_MONGO_DB_NAME= JWT_PUBLIC_KEY_PATH=./backend/jwtRS256.key.pub JWT_PRIVATE_KEY_PATH=./backend/jwtRS256.key IDP_ENABLED= -SP_HOST= \ No newline at end of file +SP_HOST= + +ANONYMIZATION_HASH_PREFIX=anon +ANONYMIZATION_DEFAULT_MINIMUM_COUNT=10 \ No newline at end of file diff --git a/src/backend/.env.test b/src/backend/.env.test index fd2b84a5b229f13218d9887d40cf0c28477a8230..74be67365bf7ef9a65b8f0df607dee29c5961188 100644 --- a/src/backend/.env.test +++ b/src/backend/.env.test @@ -14,4 +14,7 @@ JWT_PUBLIC_KEY_PATH=./backend/id_rsa.pub JWT_PRIVATE_KEY_PATH=./backend/id_rsa IDP_ENABLED=False IDP_SERVER=https://aai-test-v3.ruhr-uni-bochum.de -SP_HOST= \ No newline at end of file +SP_HOST= + +ANONYMIZATION_HASH_PREFIX=anon +ANONYMIZATION_DEFAULT_MINIMUM_COUNT=10 \ No newline at end of file diff --git a/src/backend/settings.py b/src/backend/settings.py index c47e987e5338feaa46deb6b1e332b8ebd578ef1a..635cc31ac1537a74ae31aabaa1cc75cfcfd2dc46 100644 --- a/src/backend/settings.py +++ b/src/backend/settings.py @@ -175,7 +175,13 @@ AUTH_USER_MODEL = "users.CustomUser" REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", - ) + ), + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.ScopedRateThrottle', + ], + 'DEFAULT_THROTTLE_RATES': { + 'merge': '3/hour', + } } diff --git a/src/consents/views.py b/src/consents/views.py index 0ad9122e9bfbe3a62b46c106858fa740ab14701e..fa87f584830208719c783263c6f2b01c11d7065f 100644 --- a/src/consents/views.py +++ b/src/consents/views.py @@ -261,6 +261,12 @@ class CreateProviderConsentView(APIView): ) precedingProviderSchema.superseded_by = providerSchemaModel precedingProviderSchema.save() + + provider = precedingProviderSchema.provider + provider.description = provider_schema["description"] + provider.name=provider_schema["name"] + provider.save() + except ObjectDoesNotExist: providerSchemaModel = ProviderSchema.objects.create( provider=providerModel, diff --git a/src/frontend/README.md b/src/frontend/README.md index d911bdc929e9e87d0cfe8af7ebb0c264291cc6c4..90db0dee561f1f7e6f3be6ee6fe00ae1d57da941 100644 --- a/src/frontend/README.md +++ b/src/frontend/README.md @@ -7,7 +7,7 @@ If angular cli is not already installed, you can install it with the following c npm install -g @angular/cli@latest ``` -Then, please install all package dependency's +Then, please install all package dependencies ```console $ npm install ``` diff --git a/src/frontend/src/app/app-routing.module.ts b/src/frontend/src/app/app-routing.module.ts index 446ca4f32ec854468e7803781a0628f1799d1961..6da908a718d260cb101703f0053cb132958b6201 100644 --- a/src/frontend/src/app/app-routing.module.ts +++ b/src/frontend/src/app/app-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' import { ConsentManagementComponent } from './consent-management/consent-management..component' +import { MergeDataComponent } from './merge-data/merge-data.component' import { PageNotFoundComponent } from './page-not-found/page-not-found.component' import { LoginPageComponent } from './login-page/login-page.component' import { LegalNoticeComponent } from './legal-notice/legal-notice.component' @@ -17,6 +18,7 @@ import { AnalyticsEngineComponent } from './analytics-engine/analytics-engine.co const routes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'consent-management', component: ConsentManagementComponent, canActivate: [AuthGuard] }, + { path: 'merge-actors', component: MergeDataComponent, canActivate: [AuthGuard] }, { path: 'analytics-engines', component: AnalyticsEngineComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginPageComponent }, { path: 'provider', component: ProviderComponent, canActivate: [AuthGuard] }, diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts index 604362d10f7aae29bdea02fa30b59c39ce2d1da2..0dfef99a466bb0fbeaa5685afe2992ffd538148e 100644 --- a/src/frontend/src/app/app.module.ts +++ b/src/frontend/src/app/app.module.ts @@ -43,7 +43,8 @@ import { SchemaChangeComponent } from './consent-management/schema-change/schema import { ObjectChangesComponent } from './consent-management/schema-change/object-changes/object-changes.component' import { CreateTokenDialog } from './dialogs/create-token-dialog/create-token-dialog' import { MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter'; -import { OrderByPipe } from './order-by.pipe' +import { OrderByPipe } from './order-by.pipe'; +import { MergeDataComponent } from './merge-data/merge-data.component' import { AnalyticsEngineComponent } from './analytics-engine/analytics-engine.component' import { AnalysisCard } from './analytics-engine/analysis-card/analysis-card.component' @@ -73,9 +74,10 @@ import { AnalysisCard } from './analytics-engine/analysis-card/analysis-card.com ObjectChangesComponent, CreateTokenDialog, OrderByPipe, + MergeDataComponent, AnalysisCard, AnalyticsEngineComponent - ], + ], imports: [ BrowserModule, AppRoutingModule, diff --git a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts index d3887fb8e9c6e1e9cef3d64ed2b066c2632d1b25..35f2f6471d9a1b135d44fff2a49e124b5b652542 100644 --- a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts +++ b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts @@ -110,7 +110,7 @@ export class PrivacySettingComponent implements OnInit { if (isConfirmationRequired) { const onConfirm = () => this.change.emit(this.consentDeclaration) const onCancel = () => (event.source.checked = false) - this.openVerbWarningDialog(onConfirm, onCancel) + this.openVerbWarningDialog(onConfirm, onCancel, verbId) } else this.change.emit(this.consentDeclaration) } @@ -164,11 +164,13 @@ export class PrivacySettingComponent implements OnInit { if (isConfirmationRequired) { const onConfirm = () => this.change.emit(this.consentDeclaration) const onCancel = () => (event.source.checked = false) - this.openObjectWarningDialog(onConfirm, onCancel) + this.openObjectWarningDialog(onConfirm, onCancel,verbId) } else this.change.emit(this.consentDeclaration) } - openVerbWarningDialog(onConfirm: () => void, onCancel: () => void): void { + openVerbWarningDialog(onConfirm: () => void, onCancel: () => void, verbId: string): void { + if(this.previousUserConsent && !this.getVerbConsentFromUserConsent(verbId)) + return; this._dialog.open(PauseVerbObjectWarningDialog, { disableClose: true, width: '400px', @@ -180,7 +182,9 @@ export class PrivacySettingComponent implements OnInit { }) } - openObjectWarningDialog(onConfirm: () => void, onCancel: () => void): void { + openObjectWarningDialog(onConfirm: () => void, onCancel: () => void, verbId: string): void { + if(this.previousUserConsent && !this.getVerbConsentFromUserConsent(verbId)) + return; this._dialog.open(PauseVerbObjectWarningDialog, { disableClose: true, width: '400px', diff --git a/src/frontend/src/app/consent-management/wizard/wizard.component.html b/src/frontend/src/app/consent-management/wizard/wizard.component.html index 4eae4f4f9e228f6f0934bf8e5c82852e52beeaa5..6077a49ffac4772eae07ec978a9e7e869364c763 100644 --- a/src/frontend/src/app/consent-management/wizard/wizard.component.html +++ b/src/frontend/src/app/consent-management/wizard/wizard.component.html @@ -12,6 +12,16 @@ *ngIf="this.providers.length === 0"> The provider has not yet uploaded a consent form. </h3> + + <div i18n="Guide | Guide Text @@guideText"> + <h2>Guide</h2> + <ul> + <li>Hint 1</li> + <li>Hint 2</li> + <li>Hint 3</li> + </ul> + </div> + <div *ngIf="!isSummary && selectedProviderId"> <div class="user-consents" *ngIf="wizardUserConsents[selectedProviderId] as userData"> <h2>{{ userData.provider.name }}</h2> diff --git a/src/frontend/src/app/merge-data/merge-data.component.html b/src/frontend/src/app/merge-data/merge-data.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9c7fa9ae56b839e8045fca15c1af09054e25f553 --- /dev/null +++ b/src/frontend/src/app/merge-data/merge-data.component.html @@ -0,0 +1,26 @@ +<div class="content"> + <mat-card style='margin: 8px;'> + <mat-card-title i18n="Merge Data | Header entry @@mergeDataHeader"> + Merge Data + </mat-card-title> + <mat-card-content> + + <form [formGroup]="mergeForm" (ngSubmit)="submit()"> + <mat-form-field> + <mat-label>Provider</mat-label> + <mat-select formControlName="provider"> + <mat-option *ngFor="let provider of providers" [value]="provider.id">{{ provider.name }}</mat-option> + </mat-select> + </mat-form-field> + <br> + <mat-form-field> + <mat-label>TAN</mat-label> + <input matInput formControlName="tan"> + </mat-form-field> + <br> + <button type="submit" mat-raised-button i18n="Merge Data | Header entry @@mergeDataHeader">Merge Data</button> + </form> + </mat-card-content> + </mat-card> + +</div> diff --git a/src/frontend/src/app/merge-data/merge-data.component.scss b/src/frontend/src/app/merge-data/merge-data.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/frontend/src/app/merge-data/merge-data.component.spec.ts b/src/frontend/src/app/merge-data/merge-data.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef7ac958657643e1392bf9ee0b016f8403b1108a --- /dev/null +++ b/src/frontend/src/app/merge-data/merge-data.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MergeDataComponent } from './merge-data.component'; + +describe('MergeDataComponent', () => { + let component: MergeDataComponent; + let fixture: ComponentFixture<MergeDataComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MergeDataComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MergeDataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/src/app/merge-data/merge-data.component.ts b/src/frontend/src/app/merge-data/merge-data.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..770fb191da09740ad923b8c6c99f47755097ff05 --- /dev/null +++ b/src/frontend/src/app/merge-data/merge-data.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiService, Provider } from '../services/api.service' +import { AuthService, MergeResponse } from '../services/auth.service'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { ToastrService } from 'ngx-toastr' + +@Component({ + selector: 'app-merge-data', + templateUrl: './merge-data.component.html', + styleUrls: ['./merge-data.component.scss'] +}) +export class MergeDataComponent implements OnInit { + + constructor( + private _apiService: ApiService, + private _authService: AuthService, + private _toasterService: ToastrService, + ) {} + + providers: Provider[] = [] + + mergeForm: FormGroup = new FormGroup({ + provider: new FormControl('', Validators.required), + tan: new FormControl('', Validators.required), + }); + + + submit() { + if(this.mergeForm.valid) + { + this._authService.mergeData(this.mergeForm.get('provider')!.value, this.mergeForm.get('tan')!.value).subscribe((response) => { + if(response.status == "success") + { + this._toasterService.success($localize`:@@toastMessageMerged:Merged`) + } + else + { + this._toasterService.error($localize`:@@toastMessageMergeError:Error`) + } + }); + } + } + + + getProviderList(): void { + this._apiService.getProviders().subscribe((providers) => { + this.providers = providers; + }); + } + + ngOnInit(): void { + this.getProviderList(); + } + +} diff --git a/src/frontend/src/app/navigation/header/header.component.html b/src/frontend/src/app/navigation/header/header.component.html index 0660641102724d3ccf534a6b02a1f34f8750e7f0..3127fee9cb0149d1110fb1ba9683728b5cd5c924 100644 --- a/src/frontend/src/app/navigation/header/header.component.html +++ b/src/frontend/src/app/navigation/header/header.component.html @@ -21,6 +21,14 @@ >Consent Management</a > + <a + mat-button + routerLink="/merge-actors" + routerLinkActive="mat-accent" + *ngIf="!loggedIn?.isProvider" + i18n="Merge Data | Header entry @@mergeDataHeader" + >Merge Data</a + > <a mat-button routerLink="/provider" diff --git a/src/frontend/src/app/services/auth.service.ts b/src/frontend/src/app/services/auth.service.ts index 265457af9a5af3aaa371f54ae9ee33a0a758a874..8543bc3a4c87b0c4d143d5c419f573592310aef8 100644 --- a/src/frontend/src/app/services/auth.service.ts +++ b/src/frontend/src/app/services/auth.service.ts @@ -20,6 +20,11 @@ interface DeleteResponse { message: string } +export interface MergeResponse { + status: string + message: string +} + interface Tokens { accessToken: any refreshToken: any @@ -80,7 +85,7 @@ export class AuthService { this._localStorage.remove('refreshToken') clearTimeout(this.refreshTokenTimeout) //this._router.navigateByUrl('/home') - + window.location.href = `${environment.apiUrl}/logout` } @@ -111,6 +116,11 @@ export class AuthService { return this._http.delete<DeleteResponse>(`${environment.apiUrl}/api/v1/auth/deleteUser`, {}) } + mergeData(provider: number, tan: string): Observable<MergeResponse> { + return this._http + .post<MergeResponse>(`${environment.apiUrl}/api/v1/auth/mergeData`, {provider: provider, tan: tan}); + } + private startRefreshTokenTimer() { const accessToken: any = jwt_decode(this.userSubject?.value?.accessToken) const expires = new Date(accessToken.exp * 1000) diff --git a/src/frontend/src/locale/messages.de.xlf b/src/frontend/src/locale/messages.de.xlf index 73a408ec56a7790698437fe05c8950bf3a3c2184..e2d8dc375a4febaef3b47139c27b80ea66677ac6 100644 --- a/src/frontend/src/locale/messages.de.xlf +++ b/src/frontend/src/locale/messages.de.xlf @@ -264,8 +264,19 @@ <context context-type="linenumber">104</context> </context-group> </trans-unit> - <trans-unit id="pauseDataRecordingHeader" datatype="html"> - <source>Continue Data Recording</source> + <trans-unit id="mergeDataHeader" datatype="html"> + <source>Merge Data</source> + <target>Daten verknüpfen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> + <context context-type="linenumber">21</context> + </context-group> + <note priority="1" from="description"> Header entry </note> + <note priority="1" from="meaning">Merge Data </note> + </trans-unit> + <trans-unit id="profileProfileDropDown" datatype="html"> + <source>Profile</source> + <target>Profil</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> <context context-type="linenumber">5</context> @@ -389,6 +400,7 @@ </trans-unit> <trans-unit id="pauseVerb" datatype="html"> <source>Pause Verb</source> + <target>Verb pausieren</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/provider-settings/provider-setting.component.ts</context> <context context-type="linenumber">176</context> @@ -396,6 +408,7 @@ </trans-unit> <trans-unit id="pauseObject" datatype="html"> <source>Pause Object</source> + <target>Objekt pausieren</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/provider-settings/provider-setting.component.ts</context> <context context-type="linenumber">188</context> @@ -517,8 +530,17 @@ <note priority="1" from="description"> Status </note> <note priority="1" from="meaning">Schema Added </note> </trans-unit> - <trans-unit id="schemaRemoved" datatype="html"> - <source>Schema removed</source> + <trans-unit id="toastMessageMerged" datatype="html"> + <source> Data merged</source> + <target> Daten verknüpft</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/merge-data/merge-data.component.ts</context> + <context context-type="linenumber">34</context> + </context-group> + </trans-unit> + <trans-unit id="dataRemovalCompleted" datatype="html"> + <source>Completed</source> + <source>Abgeschlossen</source> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> <context context-type="linenumber">25</context> @@ -642,6 +664,28 @@ <note priority="1" from="description"> Wizard header </note> <note priority="1" from="meaning">Consent Declaration </note> </trans-unit> + <trans-unit id="guideText" datatype="html"> + <source> <x id="START_HEADING_LEVEL2" ctype="x-h2" equiv-text="<h2>"/>Guide<x id="CLOSE_HEADING_LEVEL2" ctype="x-h2" equiv-text="</h2>"/> + <x id="START_UNORDERED_LIST" ctype="x-ul" equiv-text="<ul>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Hint 1<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Hint 2<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Hint 3<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="CLOSE_UNORDERED_LIST" ctype="x-ul" equiv-text="</ul>"/> + </source> + <target> <x id="START_HEADING_LEVEL2" ctype="x-h2" equiv-text="<h2>"/>Anleitung<x id="CLOSE_HEADING_LEVEL2" ctype="x-h2" equiv-text="</h2>"/> + <x id="START_UNORDERED_LIST" ctype="x-ul" equiv-text="<ul>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Tipp 1<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Tipp 2<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Tipp 3<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="CLOSE_UNORDERED_LIST" ctype="x-ul" equiv-text="</ul>"/> + </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">16</context> + </context-group> + <note priority="1" from="description"> Guide Text </note> + <note priority="1" from="meaning">Help </note> + </trans-unit> <trans-unit id="noProviderSchemaHint" datatype="html"> <source> The provider has not yet uploaded a consent form. </source> <context-group purpose="location"> @@ -650,6 +694,655 @@ </context-group> <note priority="1" from="description">No Provider Schema Hint </note> </trans-unit> + <trans-unit id="firstUserConsentHint" datatype="html"> + <source> You have not yet provided any information on data processing within the Polaris project. In the following steps, you can make an individual decision for each connected platform. </source> + <target> Sie haben noch keine Angaben zur Datenverarbeitung im Rahmen des Polaris-Projekts gemacht. In den folgenden Schritten können Sie eine individuelle Entscheidung für jede angeschlossene Plattform treffen. </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">14,19</context> + </context-group> + <note priority="1" from="description">First User Consent </note> + </trans-unit> + <trans-unit id="newProviderSchemaHint" datatype="html"> + <source> The provider uploaded a newer consent declaration. Please reviewed the changes. </source> + <target> Der Anbieter hat eine neuere Einverständniserklärung hochgeladen. Bitte überprüfen Sie die Änderungen. </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">23,25</context> + </context-group> + <note priority="1" from="description">New Provider Schema Hint </note> + </trans-unit> + <trans-unit id="next" datatype="html"> + <source> Next </source> + <target> Weiter </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">66,68</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">77,79</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">84,86</context> + </context-group> + <note priority="1" from="description">Next </note> + </trans-unit> + <trans-unit id="submit" datatype="html"> + <source> Submit </source> + <target> Abschicken </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">90,92</context> + </context-group> + <note priority="1" from="description">Submit </note> + </trans-unit> + <trans-unit id="deleteAccountText" datatype="html"> + <source>Delete Account</source> + <target>Account löschen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/user-profil/user-profil.component.ts</context> + <context context-type="linenumber">29</context> + </context-group> + </trans-unit> + <trans-unit id="pauseRecordingAndDeleteData" datatype="html"> + <source>Pause and Delete Confirmation</source> + <target>Pausieren und Löschen Bestätigen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/consent-management..component.ts</context> + <context context-type="linenumber">128</context> + </context-group> + </trans-unit> + <trans-unit id="essentialUserConsentsSubHeader" datatype="html"> + <source> Essential user consents </source> + <target> Unverzichtbare Einwilligungen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider-settings/provider-setting.component.html</context> + <context context-type="linenumber">86,88</context> + </context-group> + <note priority="1" from="description"> Subheader </note> + <note priority="1" from="meaning">Essential User Consents </note> + </trans-unit> + <trans-unit id="save" datatype="html"> + <source> Save </source> + <target> Speichern </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/consent-management.component.html</context> + <context context-type="linenumber">53,55</context> + </context-group> + <note priority="1" from="description">Save </note> + </trans-unit> + <trans-unit id="deleteStoredVerbData" datatype="html"> + <source> Delete Stored Data for Verb</source> + <target> Gespeicherte Daten zu Verb löschen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider-settings/provider-setting.component.ts</context> + <context context-type="linenumber">197</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider-settings/provider-setting.component.ts</context> + <context context-type="linenumber">216</context> + </context-group> + </trans-unit> + <trans-unit id="deleteStoredObjectData" datatype="html"> + <source> Delete Stored Data for Object</source> + <target> Gespeicherte Daten zu Objekt löschen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider-settings/provider-setting.component.ts</context> + <context context-type="linenumber">216</context> + </context-group> + </trans-unit> + <trans-unit id="executedAt" datatype="html"> + <source> Execute At </source> + <target> Ausgeführt am</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/data-removal/data-removal.component.html</context> + <context context-type="linenumber">43,45</context> + </context-group> + <note priority="1" from="description"> Table column header </note> + <note priority="1" from="meaning">Executed At </note> + </trans-unit> + <trans-unit id="matchedStatements" datatype="html"> + <source> Matched Statements </source> + <target> Betroffene Statements </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/data-removal/data-removal.component.html</context> + <context context-type="linenumber">53,55</context> + </context-group> + <note priority="1" from="description"> Column Header </note> + <note priority="1" from="meaning">Matched Statements </note> + </trans-unit> + <trans-unit id="description" datatype="html"> + <source>Description</source> + <target>Beschreibung</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/data-removal/data-removal.component.html</context> + <context context-type="linenumber">60</context> + </context-group> + <note priority="1" from="description">Description </note> + </trans-unit> + <trans-unit id="continueDataRecordingHeader" datatype="html"> + <source>Pause Data Recording</source> + <target> Daten Teilen Pausieren </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">5</context> + </context-group> + <note priority="1" from="description">Continue Data Recording Header </note> + </trans-unit> + <trans-unit id="pauseDataRecordingHeader" datatype="html"> + <source>Continue Data Recording</source> + <target> Daten Teilen Fortsetzen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">8</context> + </context-group> + <note priority="1" from="description">Pause Data Recording Header </note> + </trans-unit> + <trans-unit id="pausedDataRecordingDescription" datatype="html"> + <source> Would you like to continue sharing selected data with Polaris? +</source> + <target> Möchten Sie das Teilen ausgewählter Daten mit Polaris fortsetzen? </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">14,16</context> + </context-group> + <note priority="1" from="description"> Text for Pause </note> + <note priority="1" from="meaning">Pause Data Recording Description </note> + </trans-unit> + <trans-unit id="pauseRecordingAndDeleteData" datatype="html"> + <source>Pause and Delete Confirmation</source> + <target> Pausieren und Löschen Bestätigen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/consent-management..component.ts</context> + <context context-type="linenumber">128</context> + </context-group> + </trans-unit> + <trans-unit id="pauseAndDeleteDataButtonText" datatype="html"> + <source> Pause & Delete Data </source> + <target> Pausieren & Daten löschen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">39,41</context> + </context-group> + <note priority="1" from="description">Pause and Delete Data Button Text </note> + </trans-unit> + <trans-unit id="continueDataRecording" datatype="html"> + <source>Continue</source> + <target>Fortsetzen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">30</context> + </context-group> + <note priority="1" from="description">Continue Data Recording </note> + </trans-unit> + <trans-unit id="pauseDataRecording" datatype="html"> + <source>Pause</source> + <target>Pausieren</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">34</context> + </context-group> + <note priority="1" from="description">Pause Data Recording </note> + </trans-unit> + <trans-unit id="continueDataRecordingDescription" datatype="html"> + <source> Would you like to pause sharing selected data with Polaris? <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b + >"/>A delete job is set up for data deletion, which is only executed after a fixed time window.<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b + >"/></source> + <target> Möchten Sie die Erfassung ihrer Daten durch Polaris pausieren? <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b + >"/> Für die Datenlöschung wird ein Löschautrag eingerichtet, welcher erst nach einem festen Zeitfenster ausgeführt wird. <x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b + >"/> </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> + <context context-type="linenumber">19,24</context> + </context-group> + <note priority="1" from="description"> Text for Continue </note> + <note priority="1" from="meaning">Continue Data Recording Description </note> + </trans-unit> + <trans-unit id="applicationTokens" datatype="html"> + <source>Application Tokens</source> + <target>Anwendungs-Tokens</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> + <context context-type="linenumber">28</context> + </context-group> + <note priority="1" from="description"> Header Entry </note> + <note priority="1" from="meaning">Application Tokens </note> + </trans-unit> + <trans-unit id="supersededBy" datatype="html"> + <source> Superseded by: <x id="INTERPOLATION" equiv-text="{{ consent.superseded_by }}"/> </source> + <target> Ersetzt durch: <x id="INTERPOLATION" equiv-text="{{ consent.superseded_by }}"/> </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.html</context> + <context context-type="linenumber">9,11</context> + </context-group> + <note priority="1" from="description"> Provider schema information </note> + <note priority="1" from="meaning">Superseded By </note> + </trans-unit> + <trans-unit id="toggleDefinition" datatype="html"> + <source>Toggle Definition</source> + <target> Definiton Ein/Ausblenden </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.html</context> + <context context-type="linenumber">15</context> + </context-group> + <note priority="1" from="description"> Slide Toggle Description </note> + <note priority="1" from="meaning">Toggle Definition </note> + </trans-unit> + <trans-unit id="createProviderSchema" datatype="html"> + <source>Create/Update Provider Schema</source> + <target>Provider-Schema Erstellen/Aktualsieren</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.html</context> + <context context-type="linenumber">25</context> + </context-group> + <note priority="1" from="description">Create Provider Schema </note> + </trans-unit> + <trans-unit id="providerSchemaNameRequiredHint" datatype="html"> + <source> Name is <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>required<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></source> + <target> Name ist <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>erforderlich<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.html</context> + <context context-type="linenumber">32,33</context> + </context-group> + <note priority="1" from="description">Provider Schema Name Required Hint </note> + </trans-unit> + <trans-unit id="submit" datatype="html"> + <source> Submit </source> + <source> Absenden </source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.html</context> + <context context-type="linenumber">68,70</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">88,90</context> + </context-group> + <note priority="1" from="description">Submit </note> + </trans-unit> + <trans-unit id="selectProviderSchema" datatype="html"> + <source>Please select a JSON file.</source> + <target>Bitte wählen Sie eine JSON-Datei aus.</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.ts</context> + <context context-type="linenumber">19</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/provider/provider.component.ts</context> + <context context-type="linenumber">101</context> + </context-group> + </trans-unit> + <trans-unit id="inputRequired" datatype="html"> + <source> Input is <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>required<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></source> + <target> Eingabe ist <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>erforderlich<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/dialogs/delete-dialog/delete-dialog.html</context> + <context context-type="linenumber">22,23</context> + </context-group> + <note priority="1" from="description"> Error input required text </note> + <note priority="1" from="meaning">Input required </note> + </trans-unit> + <trans-unit id="createAnalyticsToken" datatype="html"> + <source> Create Token </source> + <target> Token Generieren </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/analytics-tokens/analytics-tokens.component.html</context> + <context context-type="linenumber">10,12</context> + </context-group> + <note priority="1" from="description"> Button Text </note> + <note priority="1" from="meaning">Create Token </note> + </trans-unit> + <trans-unit id="tokenCreated" datatype="html"> + <source> Token created</source> + <target> Token Angelegt </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/analytics-tokens/analytics-tokens.component.ts</context> + <context context-type="linenumber">75</context> + </context-group> + </trans-unit> + <trans-unit id="headerVisualizationTokens" datatype="html"> + <source> Visualization Tokens </source> + <target> Visualisierungs-Token </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/visualization-tokens/visualization-tokens.component.html</context> + <context context-type="linenumber">4,6</context> + </context-group> + <note priority="1" from="description"> Header text visualization tokens page </note> + <note priority="1" from="meaning">Visualization Tokens </note> + </trans-unit> + <trans-unit id="visualizationTokens" datatype="html"> + <source>Visualization Tokens</source> + <target> Visualisierungs-Token </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> + <context context-type="linenumber">44</context> + </context-group> + <note priority="1" from="description"> Header Entry </note> + <note priority="1" from="meaning">Visualization Tokens </note> + </trans-unit> + <trans-unit id="optionalExpirationDate" datatype="html"> + <source>Optional Expiration Date</source> + <target>Optionales Ablaufdatum</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/dialogs/create-token-dialog/create-token-dialog.html</context> + <context context-type="linenumber">7</context> + </context-group> + <note priority="1" from="description"> Input Field Header </note> + <note priority="1" from="meaning">Optional Expiration Date </note> + </trans-unit> + <trans-unit id="invalidExpirationDate" datatype="html"> + <source> Expiration date needs to be at least 5 days in the future. </source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/dialogs/create-token-dialog/create-token-dialog.html</context> + <context context-type="linenumber">21,23</context> + </context-group> + <note priority="1" from="description">Invalid Expiration Date </note> + </trans-unit> + <trans-unit id="availbaleVerbsHeader" datatype="html"> + <source> Available Verbs </source> + <target> Verfügbare Verben </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.html</context> + <context context-type="linenumber">83,85</context> + </context-group> + <note priority="1" from="description"> Table Expanded Row Header + </note> + <note priority="1" from="meaning">Available Verbs </note> + </trans-unit> + <trans-unit id="invalidExpirationDate" datatype="html"> + <source> Expiration date needs to be at least 5 days in the future. </source> + <target> Ablaufdatum muss mindestens 5 Tage in der Zukunft liegen. </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/dialogs/create-token-dialog/create-token-dialog.html</context> + <context context-type="linenumber">21,23</context> + </context-group> + <note priority="1" from="description">Invalid Expiration Date </note> + </trans-unit> + <trans-unit id="createAnalyticsTokenDialogHeader" datatype="html"> + <source>Create New Analytics Token</source> + <target>Neues Analyse-Token Erstellen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.ts</context> + <context context-type="linenumber">75</context> + </context-group> + </trans-unit> + <trans-unit id="copyToken" datatype="html"> + <source> Copy Token </source> + <target> Token Kopieren </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.html</context> + <context context-type="linenumber">70,72</context> + </context-group> + <note priority="1" from="description">Copy Token </note> + </trans-unit> + <trans-unit id="deleteToken" datatype="html"> + <source> Delete Token </source> + <target> Token Löschen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.html</context> + <context context-type="linenumber">77,79</context> + </context-group> + <note priority="1" from="description">Delete Token </note> + </trans-unit> + <trans-unit id="save" datatype="html"> + <source> Save </source> + <target> Speichern </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.html</context> + <context context-type="linenumber">99,101</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/consent-management.component.html</context> + <context context-type="linenumber">47,49</context> + </context-group> + <note priority="1" from="description">Save </note> + </trans-unit> + <trans-unit id="deleteSelectedToken" datatype="html"> + <source>Delete selected token</source> + <target>Ausgewähltes Token löschen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.ts</context> + <context context-type="linenumber">115</context> + </context-group> + </trans-unit> + <trans-unit id="deleteDataDisclosure" datatype="html"> + <source>Delete data disclosure</source> + <target>Datenauskunft löschen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="linenumber">69</context> + </context-group> + </trans-unit> + <trans-unit id="deleteDataDisclosureSucces" datatype="html"> + <source>Data disclosure deleted</source> + <target>Datenauskunft wurde gelöscht</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="linenumber">73</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChanges" datatype="html"> + <source> Schema Changes </source> + <target> Schema-Änderungen </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">3,5</context> + </context-group> + <note priority="1" from="description"> Header </note> + <note priority="1" from="meaning">Schema Changes </note> + </trans-unit> + <trans-unit id="schemaChangeSubheader" datatype="html"> + <source> Changes between <x id="INTERPOLATION" equiv-text="{{ oldSchema?.name }}"/> and <x id="INTERPOLATION_1" equiv-text="{{ newSchema?.name }}"/> </source> + <target> Schema-Änderungen zwischen <x id="INTERPOLATION" equiv-text="{{ oldSchema?.name }}"/> und <x id="INTERPOLATION_1" equiv-text="{{ newSchema?.name }}"/></target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">6,8</context> + </context-group> + <note priority="1" from="description"> Subheader </note> + <note priority="1" from="meaning">Schema Changes </note> + </trans-unit> + <trans-unit id="changedVerbs" datatype="html"> + <source> Changed Verbs </source> + <target> Geänderte Verben </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">24,26</context> + </context-group> + <note priority="1" from="description"> Sub header </note> + <note priority="1" from="meaning">Changed Verbs </note> + </trans-unit> + <trans-unit id="removedVerbs" datatype="html"> + <source> Removed Verbs </source> + <target> Entfernte Verben </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">57,59</context> + </context-group> + <note priority="1" from="description"> Sub header </note> + <note priority="1" from="meaning">Removed Verbs </note> + </trans-unit> + <trans-unit id="objectsInVerbChanged" datatype="html"> + <source> Objects in Verb changed </source> + <target> Objekte in Verb Änderungen</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">76,78</context> + </context-group> + <note priority="1" from="description"> Sub header </note> + <note priority="1" from="meaning">Objects in Verb changed </note> + </trans-unit> + <trans-unit id="schemaChangeVerbObjChanges" datatype="html"> + <source>Verb <x id="PH" equiv-text="newVerb.label"/> includes object changes</source> + <target>Verb <x id="PH" equiv-text="newVerb.label"/> enthält Änderungen am Objekt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">110</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeVerbOptInToOptOut" datatype="html"> + <source>Verb <x id="PH" equiv-text="newVerb.label"/> changed from Opt In to Opt Out</source> + <target>Verb <x id="PH" equiv-text="newVerb.label"/> wechselt von Opt In zu Opt Out</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">120</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeVerbOptOutToOptIn" datatype="html"> + <source>Verb <x id="PH" equiv-text="newVerb.label"/> changed from Opt Out to Opt In</source> + <target>Verb <x id="PH" equiv-text="newVerb.label"/> wechselt von Opt Out zu Opt In</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">129</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeVerbAdded" datatype="html"> + <source>Verb <x id="PH" equiv-text="newSchemaVerb.label"/> added</source> + <target>Verb <x id="PH" equiv-text="newSchemaVerb.label"/> hinzugefügt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">156</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">229</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">240</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeVerbRemoved" datatype="html"> + <source>Essential Verb <x id="PH" equiv-text="oldVerb.label"/> removed</source> + <target>Unverzichtbares Verb <x id="PH" equiv-text="oldVerb.label"/> entfernt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">172</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">262</context> + </context-group> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">273</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeEssentialVerbAdded" datatype="html"> + <source>Essential Verb <x id="PH" equiv-text="newEssentialVerb.label"/> added</source> + <target>Unverzichtbares Verb <x id="PH" equiv-text="newEssentialVerb.label"/> hinzugefügt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">196</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeEssentialVerbRemoved" datatype="html"> + <source>Essential Verb <x id="PH" equiv-text="oldEssentialVerb.label"/> removed</source> + <target>Unverzichtbares Verb <x id="PH" equiv-text="oldEssentialVerb.label"/> entfernt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">213</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChanged" datatype="html"> + <source>Schema changed</source> + <target>Schema geändert</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">18</context> + </context-group> + <note priority="1" from="description"> Status </note> + <note priority="1" from="meaning">Schema Changed </note> + </trans-unit> + <trans-unit id="schemaChangeVerbMovedGroup" datatype="html"> + <source>Verb <x id="PH" equiv-text="verb.label"/> moved from group "<x id="PH_1" equiv-text="groupOldSchema.label"/>" to "<x id="PH_2" equiv-text="otherGroup.label"/>"</source> + <target>Verb <x id="PH" equiv-text="verb.label"/> aus Gruppe "<x id="PH_1" equiv-text="groupOldSchema.label"/>" in Gruppe "<x id="PH_2" equiv-text="otherGroup.label"/>" verschoben</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">325</context> + </context-group> + </trans-unit> + <trans-unit id="schemaChangeVerbMovedGroupAlt" datatype="html"> + <source>Verb <x id="PH" equiv-text="verb.label"/> moved to group "<x id="PH_1" equiv-text="otherGroup.label"/>"</source> + <target>Verb <x id="PH" equiv-text="verb.label"/> in Gruppe "<x id="PH_1" equiv-text="otherGroup.label"/>" verschoben</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.ts</context> + <context context-type="linenumber">343</context> + </context-group> + </trans-unit> + <trans-unit id="schemaAdded" datatype="html"> + <source>Schema added</source> + <target>Schema hinzugefügt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">23</context> + </context-group> + <note priority="1" from="description"> Status </note> + <note priority="1" from="meaning">Schema Added </note> + </trans-unit> + <trans-unit id="schemaRemoved" datatype="html"> + <source>Schema removed</source> + <target>Schema entfernt</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/schema-change/schema-change.component.html</context> + <context context-type="linenumber">28</context> + </context-group> + <note priority="1" from="description"> Status </note> + <note priority="1" from="meaning">Schema Removed </note> + </trans-unit> + <trans-unit id="tokenDeleted" datatype="html"> + <source> Token deleted</source> + <target> Token gelöscht</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.ts</context> + <context context-type="linenumber">123</context> + </context-group> + </trans-unit> + <trans-unit id="prevProvider" datatype="html"> + <source>Previous Provider</source> + <target>Vorheriger Provider</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">93</context> + </context-group> + <note priority="1" from="description">Previous Provider </note> + </trans-unit> + <trans-unit id="nextProvider" datatype="html"> + <source>Next Provider</source> + <target>Nächster Provider</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">96</context> + </context-group> + <note priority="1" from="description">Next Provider </note> + </trans-unit> + <trans-unit id="summary" datatype="html"> + <source>Summary</source> + <target>Zusammenfassung</target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">98</context> + </context-group> + <note priority="1" from="description">Summary </note> + </trans-unit> + <trans-unit id="firstConsentDeclaration" datatype="html"> + <source> You have not yet given consent for provider <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/>. </source> + <target> Sie haben bisher noch keine Zustimmung für den Provider <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/> gegeben. </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">46,48</context> + </context-group> + <note priority="1" from="description">First Consent Declaration </note> + </trans-unit> + <trans-unit id="consentDeclarationUpToDate" datatype="html"> + <source> You do not need to make any changes to your existing consent form, as the provider has not made any changes in the meantime. </source> + <target> Du musst keine Änderungen an deiner bestehenden Einwilligungserklärung treffen, da der Provider in der Zwischenzeit keine Änderungen vorgenommen hat. </target> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">41,44</context> + </context-group> + <note priority="1" from="description">Consent Declaration UpToDate </note> + </trans-unit> <trans-unit id="providerSchemaChanged" datatype="html"> <source> Since your last visit, the provider has made changes. For each option, you will now see your previous consent on the far left, and on the right, separated by an arrow, your current consent, which you can of course change. </source> <context-group purpose="location"> diff --git a/src/frontend/src/locale/messages.xlf b/src/frontend/src/locale/messages.xlf index 73a408ec56a7790698437fe05c8950bf3a3c2184..197fba2c6314644d2cd41f3282ad13ad1a7e4c23 100644 --- a/src/frontend/src/locale/messages.xlf +++ b/src/frontend/src/locale/messages.xlf @@ -257,6 +257,20 @@ <context context-type="linenumber">115</context> </context-group> </trans-unit> + <trans-unit id="toastMessageMerged" datatype="html"> + <source>Data merged</source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/merge-data/merge-data.component.ts</context> + <context context-type="linenumber">34</context> + </context-group> + </trans-unit> + <trans-unit id="toastMessageMergeError" datatype="html"> + <source> Error. Merged 0 entries.</source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/merge-data/merge-data.component.ts</context> + <context context-type="linenumber">37</context> + </context-group> + </trans-unit> <trans-unit id="pauseRecordingAndDeleteData" datatype="html"> <source>Pause and Delete Confirmation</source> <context-group purpose="location"> @@ -633,8 +647,7 @@ </context-group> </trans-unit> <trans-unit id="consentDeclaration" datatype="html"> - <source> Consent Declaration -</source> + <source> Consent Declaration</source> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">1,3</context> @@ -642,6 +655,21 @@ <note priority="1" from="description"> Wizard header </note> <note priority="1" from="meaning">Consent Declaration </note> </trans-unit> + <trans-unit id="guideText" datatype="html"> + <source> <x id="START_HEADING_LEVEL2" ctype="x-h2" equiv-text="<h2>"/>Guide<x id="CLOSE_HEADING_LEVEL2" ctype="x-h2" equiv-text="</h2>"/> + <x id="START_UNORDERED_LIST" ctype="x-ul" equiv-text="<ul>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Hint 1<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Hint 2<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="START_LIST_ITEM" ctype="x-li" equiv-text="<li>"/>Hint 3<x id="CLOSE_LIST_ITEM" ctype="x-li" equiv-text="</li>"/> + <x id="CLOSE_UNORDERED_LIST" ctype="x-ul" equiv-text="</ul>"/> + </source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> + <context context-type="linenumber">16</context> + </context-group> + <note priority="1" from="description"> Wizard header </note> + <note priority="1" from="meaning">Consent Declaration </note> + </trans-unit> <trans-unit id="noProviderSchemaHint" datatype="html"> <source> The provider has not yet uploaded a consent form. </source> <context-group purpose="location"> @@ -1087,6 +1115,15 @@ <note priority="1" from="description"> Header entry </note> <note priority="1" from="meaning">Consent Management </note> </trans-unit> + <trans-unit id="mergeDataHeader" datatype="html"> + <source>Merge Data</source> + <context-group purpose="location"> + <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> + <context context-type="linenumber">20</context> + </context-group> + <note priority="1" from="description"> Header entry </note> + <note priority="1" from="meaning">Merge Data </note> + </trans-unit> <trans-unit id="applicationTokens" datatype="html"> <source>Application Tokens</source> <context-group purpose="location"> diff --git a/src/static/provider_schema.schema.json b/src/static/provider_schema.schema.json index 2b7bda4167546177e4886ee1a3c930189def1b44..c017ac54326b3b5a4465838267bed23f82f0123d 100644 --- a/src/static/provider_schema.schema.json +++ b/src/static/provider_schema.schema.json @@ -50,6 +50,12 @@ "defaultConsent": { "type": "boolean" }, + "allowAnonymizedCollection": { + "type": "boolean" + }, + "allowAnonymizedCollectionMinCount": { + "type": "integer" + }, "objects": { "type": "array", "items": [ diff --git a/src/users/urls.py b/src/users/urls.py index b6905dd2084183e9ce648fb2f77092fdbfeb6bbd..2fee4242e58ccb6db51fa3f6f311dde92282dc4b 100644 --- a/src/users/urls.py +++ b/src/users/urls.py @@ -11,4 +11,5 @@ urlpatterns = [ path("createUser", views.CreateUserView.as_view()), path("toggle-data-recording", views.ToggleDataRecording.as_view()), path("deleteUser", views.DeleteUserView.as_view()), + path("mergeData", views.MergeDataView.as_view()), ] diff --git a/src/users/views.py b/src/users/views.py index 7774c043c697a3b6c60c54b33ced8616219ca15c..f868efb08f85b2b5acd03f4f190218521fdc8572 100644 --- a/src/users/views.py +++ b/src/users/views.py @@ -12,6 +12,7 @@ from rolepermissions.checkers import has_permission from rolepermissions.roles import assign_role from backend.roles import Roles +from backend.utils import lrs_db from consents.views import JsonUploadParser from data_removal.models import DataRemovalJob @@ -113,3 +114,41 @@ class DeleteUserView(APIView): self.send_email(user) return JsonResponse({"message": "user deleted"}, status=status.HTTP_200_OK) + +class MergeDataView(APIView): + permission_classes = (IsAuthenticated,) + throttle_scope = 'merge' + + def post(self, request): + """ + Allows a user to merge data given a provider and TAN. Rate limited (see settings.py). + """ + user = request.user + + tan = request.data.get("tan", None) + provider_id = request.data.get("provider", None) + + if tan == None or provider_id == None: + return JsonResponse( + {"status": "error", "message": "invalid form data"}, status=status.HTTP_400_BAD_REQUEST + ) + + collection = lrs_db["statements"] + + query = { + "$and": [ + {"actor.tan": {"$exists": True, "$eq": tan}}, + {"actor.provider_id": {"$exists": True, "$eq": provider_id}}, + ] + } + + result = collection.update_many(query, + { + "$unset": {"actor.tan": "", "actor.provider_id": ""}, + "$set": {"actor.mbox": f'mailto:{user.email}'} + }) + + if result.modified_count > 0: + return JsonResponse({"status": "success", "message": f'{result.modified_count} records updated'}, status=status.HTTP_200_OK) + else: + return JsonResponse({"status": "error", "message": f'0 records updated'}, status=status.HTTP_400_BAD_REQUEST) diff --git a/src/xapi/urls.py b/src/xapi/urls.py index 81c189b68cf6339f9391ff1a930090f9f8e5ff82..97dbbd2a4cbb43ab0d329f06916fd62e2119f324 100644 --- a/src/xapi/urls.py +++ b/src/xapi/urls.py @@ -3,5 +3,6 @@ from django.urls import path from . import views urlpatterns = [ - path('statements', views.CreateStatement.as_view()) -] \ No newline at end of file + path('statements', views.CreateStatement.as_view()), + path('tanStatements', views.CreateTANStatement.as_view()) +] diff --git a/src/xapi/views.py b/src/xapi/views.py index cb4d590786272816911e32c33bd7a415d99a731b..3c4550f7f427d37a8ecb7d95815f65241f97a060 100644 --- a/src/xapi/views.py +++ b/src/xapi/views.py @@ -1,3 +1,4 @@ +import hashlib import json import os from venv import logger @@ -32,6 +33,20 @@ def store_in_db(x_api_statement): return None +def anonymize_statement(x_api_statement): + mbox = dict( + enumerate(x_api_statement.get("actor", {}).get("mbox", "").split("mailto:")) + ).get(1) + account_email = ( + x_api_statement.get("actor", {}).get("account", {}).get("name", None) + ) + email = mbox if mbox else account_email + + hashed_actor = hashlib.sha3_512(email.encode('utf-8')).hexdigest() + + x_api_statement.set("actor", {"name": "anonymous", "mbox": settings.ANON_HASH_PREFIX + ": " + hashed_actor}) + + def process_statement(x_api_statement, provider, latest_schema): """ Process xAPI statement by checking for validation errors and user consent settings. @@ -70,6 +85,9 @@ def process_statement(x_api_statement, provider, latest_schema): # essential verbs do not require consent if not verb in [verb["id"] for verb in latest_schema.essential_verbs]: + + anon_verbs = [verb["id"] for verblist in [group["verbs"] for group in latest_schema.groups] for verb in verblist if verb.get("allowAnonymizedCollection", False)] + # has the user paused data collection altogether? if user.paused_data_recording: return { @@ -85,6 +103,13 @@ def process_statement(x_api_statement, provider, latest_schema): ).first() if not user_consent: + if verb in anon_verbs: + return { + "valid": True, + "accepted": True, + "needs_anonymization": True, + } + return { "valid": True, "accepted": False, @@ -113,6 +138,12 @@ def process_statement(x_api_statement, provider, latest_schema): object_consent = True if not object_consent: + if verb in anon_verbs: + return { + "valid": True, + "accepted": True, + "needs_anonymization": True, + } return { "valid": True, "accepted": False, @@ -121,6 +152,34 @@ def process_statement(x_api_statement, provider, latest_schema): return {"valid": True, "accepted": True} +def process_tan_statement(x_api_statement): + """ + Process xAPI statement by checking for validation errors. + """ + try: + validate(x_api_statement, x_api_statement) + except ValidationError as e: + return {"valid": False, "reason": e.message} + + + # expects the tan to be in a field of the actor named "tan" + tan = x_api_statement.get("actor", {}).get("tan", None) + + if tan is None: + return { + "valid": False, + "reason": "TAN missing in statement", + } + + if (not "verb" in x_api_statement) or (not "id" in x_api_statement["verb"]): + return {"valid": False, "reason": "No verb given"} + verb = x_api_statement["verb"]["id"] + + if (not "object" in x_api_statement) or (not "id" in x_api_statement["object"]): + return {"valid": False, "reason": "No object given"} + + return {"valid": True} + class CreateStatement(APIView): """ @@ -199,6 +258,81 @@ class CreateStatement(APIView): status=status.HTTP_400_BAD_REQUEST, ) else: + # anonymize statements + x_api_statements = [anonymize_statement(statement) if result[i].get("needs_anonymization", False) + else statement for i, statement in enumerate(x_api_statements)] + uuids = list(map(store_in_db, x_api_statements)) + return JsonResponse( + { + "message": "xAPI statements successfully stored in LRS", + "data": uuids, + }, + safe=False, + status=status.HTTP_200_OK, + ) + + +class CreateTANStatement(APIView): + """ + xAPI create statements proxy for use with TANs. Stores all statements which contain TANs and are authorized with a provider token. + """ + + def post(self, request): + # provider authorization + auth_header = request.headers.get("Authorization") + if not auth_header.startswith("Basic "): + return JsonResponse( + { + "message": "No provider authorization token supplied.", + "provider": "not found", + }, + safe=False, + status=status.HTTP_401_UNAUTHORIZED, + ) + auth_key = auth_header.split(" ")[1] + try: + provider_auth = ProviderAuthorization.objects.get(key=auth_key) + except ObjectDoesNotExist: + return JsonResponse( + { + "message": "No provider for authorization token found.", + "provider": "not found", + }, + safe=False, + status=status.HTTP_400_BAD_REQUEST, + ) + + provider = provider_auth.provider + + # handle list of xAPI statements as well as single xAPI statement + x_api_statements = ( + request.data if isinstance(request.data, list) else [request.data] + ) + + result = [ + process_tan_statement(stmt) + for stmt in x_api_statements + ] + + invalid = ( + len([e for e in result if e["valid"] == False]) + > 0 + ) + + if invalid: + return JsonResponse( + { + "message": "xAPI statements couldn't be stored in LRS", + "data": result, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + # store provider as well, so statements can be properly assigned when searched by users + for index, statement in enumerate(x_api_statements): + statement["actor"]["provider_id"] = provider.id + x_api_statements[index] = statement + uuids = list(map(store_in_db, x_api_statements)) return JsonResponse( { diff --git a/tools/xapi-statement-generator/README.md b/tools/xapi-statement-generator/README.md index 6e87838bd125a95ddd13ca3c10082c49484c1473..8cc78c337f6f1cf655faa2558c677cb0dc15e0f9 100644 --- a/tools/xapi-statement-generator/README.md +++ b/tools/xapi-statement-generator/README.md @@ -1,6 +1,6 @@ # xAPI Statement Generator -Helper tool that randomly generates xAPI statements or importes xAPI statements form a JSON file. The tranformed xAPI statements are sent to an xAPI endpoint (LRS/Rights Engine). +Helper tool that randomly generates xAPI statements or imports xAPI statements from a JSON file. The transformed xAPI statements are sent to an xAPI endpoint (LRS/Rights Engine). ## Usage @@ -22,4 +22,4 @@ $ python generator.py -t http://localhost:8003/xapi/statements -r ## Application Tokens -Application tokens are accessible for privileged polaris users on the polaris website and limited to a single provider. Without a valid token, requests are rejected by the polaris backend. \ No newline at end of file +Application tokens are accessible for privileged polaris users on the polaris website and limited to a single provider. Without a valid token, requests are rejected by the polaris backend. diff --git a/tools/xapi-statement-generator/faker.py b/tools/xapi-statement-generator/faker.py index 044b695349eaef8bcafac97a3d3f7a9458c85519..04f33d5ef16b75ace9450c28feacd5f233d0e882 100644 --- a/tools/xapi-statement-generator/faker.py +++ b/tools/xapi-statement-generator/faker.py @@ -13,7 +13,7 @@ class XApiFaker: except IOError: print("Couldn't read provider_config.json.") - def fake_statement(self): + def fake_statement(self, tan = ""): providers = self.read_provider_config_file() provider_config = random.choice(providers) @@ -24,12 +24,17 @@ class XApiFaker: xapi_object = random.choice(verb_config["objects"]) now = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") - actor = random.choice(provider_config["actors"]) + actor_obj = {} + if tan == "": + actor = random.choice(provider_config["actors"]) + actor_obj = {"mbox": f"mailto:{actor}"} + else: + actor_obj = {"tan": f'{tan}', "provider_id": 1} return { "token": provider_config["token"], "provider_name": provider_config["name"], "statement": { - "actor": {"mbox": f"mailto:{actor}"}, + "actor": actor_obj, "verb": xapi_verb, "object": xapi_object, "timestamp": now, diff --git a/tools/xapi-statement-generator/generator.py b/tools/xapi-statement-generator/generator.py index 5d30db2ba2e1445ff7cb746a8cc7089e713c7f4c..5f8678033b5b464bffb81872018d4270dc966520 100644 --- a/tools/xapi-statement-generator/generator.py +++ b/tools/xapi-statement-generator/generator.py @@ -102,12 +102,13 @@ def import_json(args): def start_random_generation(args): url = args.target[0] + tan = args.use_tan if args.use_tan is not None else "" print("Running...") statistic = Statistic() faker = XApiFaker() while True: try: - data = faker.fake_statement() + data = faker.fake_statement(tan) send_x_api_statement(data["token"], statistic, url, data["statement"], data["provider_name"]) time.sleep(0.2) except KeyboardInterrupt: @@ -165,6 +166,13 @@ def main(): help="Creates and sends a random xAPI statement to an LRS in an interval of 1/s.", ) + parser.add_argument( + "-u", + "--use_tan", + nargs='?', + help="Only in combination with -r: instead of a user (email), store a TAN and provider ID 1.", + ) + args = parser.parse_args() if (