Skip to content

Commit cc0f6aa

Browse files
FrancescoMolinaroatarix83
authored andcommitted
Merged in gdpr-metrics_2023_02_x_DSC-1413 (pull request DSpace#1253)
Gdpr metrics 2023 02 x DSC-1413 Approved-by: Giuseppe Digilio
2 parents 44d80ba + 317b152 commit cc0f6aa

17 files changed

Lines changed: 330 additions & 42 deletions

src/app/shared/cookies/browser-klaro.service.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import { Inject, Injectable, InjectionToken } from '@angular/core';
2-
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
2+
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
33
import { AuthService } from '../../core/auth/auth.service';
44
import { TranslateService } from '@ngx-translate/core';
55
import { environment } from '../../../environments/environment';
66
import { map, switchMap, take } from 'rxjs/operators';
77
import { EPerson } from '../../core/eperson/models/eperson.model';
8-
import { KlaroService } from './klaro.service';
8+
import { CookieConsents, KlaroService } from './klaro.service';
99
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
1010
import { CookieService } from '../../core/services/cookie.service';
1111
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
1212
import cloneDeep from 'lodash/cloneDeep';
1313
import debounce from 'lodash/debounce';
1414
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
15-
import { Operation } from 'fast-json-patch';
15+
import { deepClone, Operation } from 'fast-json-patch';
1616
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
1717
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
1818
import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
19+
import isEqual from 'lodash/isEqual';
1920

2021
/**
2122
* Metadata field to store a user's cookie consent preferences in
@@ -65,11 +66,19 @@ export class BrowserKlaroService extends KlaroService {
6566

6667
private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';
6768

69+
private lastCookiesConsents: CookieConsents;
70+
6871
/**
6972
* Initial Klaro configuration
7073
*/
7174
klaroConfig = cloneDeep(klaroConfiguration);
7275

76+
/**
77+
* Subject to emit updates in the consents
78+
*/
79+
consentsUpdates$: BehaviorSubject<CookieConsents> = new BehaviorSubject<CookieConsents>(null);
80+
81+
7382
constructor(
7483
private translateService: TranslateService,
7584
private authService: AuthService,
@@ -94,6 +103,20 @@ export class BrowserKlaroService extends KlaroService {
94103
this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
95104
}
96105

106+
if (hasValue(environment.info.metricsConsents)) {
107+
environment.info.metricsConsents.forEach((metric) => {
108+
if (metric.enabled) {
109+
this.klaroConfig.services.push(
110+
{
111+
name: metric.key,
112+
purposes: ['thirdPartyJs'],
113+
required: false,
114+
}
115+
);
116+
}
117+
});
118+
}
119+
97120
const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
98121
getFirstCompletedRemoteData(),
99122
map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
@@ -331,6 +354,23 @@ export class BrowserKlaroService extends KlaroService {
331354
return 'klaro-' + identifier;
332355
}
333356

357+
watchConsentUpdates(): void {
358+
this.lazyKlaro.then(({getManager}) => {
359+
const manager = getManager(this.klaroConfig);
360+
const consentsSubject$ = this.consentsUpdates$;
361+
let lastCookiesConsents = this.lastCookiesConsents;
362+
manager.watch({
363+
update(_, eventName, consents) {
364+
365+
if (eventName === 'consents' && !isEqual(consents, lastCookiesConsents)) {
366+
lastCookiesConsents = deepClone(consents);
367+
consentsSubject$.next(consents);
368+
}
369+
}
370+
});
371+
});
372+
}
373+
334374
/**
335375
* remove the google analytics from the services
336376
*/
Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Injectable } from '@angular/core';
22

3-
import { Observable } from 'rxjs';
4-
3+
import { BehaviorSubject, Observable } from 'rxjs';
4+
export interface CookieConsents {
5+
[key: string]: boolean;
6+
}
57
/**
68
* Abstract class representing a service for handling Klaro consent preferences and UI
79
*/
@@ -10,15 +12,25 @@ export abstract class KlaroService {
1012
/**
1113
* Initializes the service
1214
*/
13-
abstract initialize();
15+
abstract initialize(): void;
1416

1517
/**
1618
* Shows a dialog with the current consent preferences
1719
*/
18-
abstract showSettings();
20+
abstract showSettings(): void;
1921

2022
/**
2123
* Return saved preferences stored in the klaro cookie
2224
*/
2325
abstract getSavedPreferences(): Observable<any>;
26+
27+
/**
28+
* Watch for changes in consents
29+
*/
30+
abstract watchConsentUpdates(): void;
31+
32+
/**
33+
* Subject to emit updates in the consents
34+
*/
35+
abstract consentsUpdates$: BehaviorSubject<CookieConsents>;
2436
}

src/app/shared/metric/metric-altmetric/metric-altmetric.component.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
<div class="row d-flex align-items-center"
2-
*ngIf="!failed && !(isHidden$ | async) && (remark | dsListMetricProps: 'data-badge-enabled':isListElement == true)">
2+
*ngIf="(!failed &&
3+
canLoadScript &&
4+
!(isHidden$ | async) &&
5+
(remark | dsListMetricProps: 'data-badge-enabled':isListElement == true))"
6+
>
37
<div class="col-5 text-left">
48
<div #metricChild>
59
<div
610
class="altmetric-embed"
7-
[attr.data-hide-no-mentions]="remark | dsListMetricProps : 'data-hide-no-mentions' : isListElement"
11+
[attr.data-hide-no-mentions]="visibleWithoutData ? false : (remark | dsListMetricProps : 'data-hide-no-mentions' : isListElement)"
812
[attr.data-hide-less-than]="remark | dsListMetricProps : 'data-hide-less-than' : isListElement"
913
[attr.data-badge-details]="remark | dsListMetricProps : 'data-badge-details' : isListElement"
1014
[attr.data-badge-type]="remark | dsListMetricProps : 'badgeType' : isListElement"
@@ -21,3 +25,9 @@
2125
</div>
2226
</div>
2327
</div>
28+
<div class="row d-flex align-items-center justify-content-center m-2" *ngIf="!canLoadScript && !isListElement">
29+
<div>
30+
{{ "third-party-metrics-cookies.message" | translate: {metricType: metric.metricType | titlecase} }}
31+
<div role="button" class="btn-link" (click)="requestSettingsConsent.emit(true)">{{"third-party-metrics-cookies.consent-settings" | translate}}</div>
32+
</div>
33+
</div>

src/app/shared/metric/metric-dimensions/metric-dimensions.component.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
<div class="row d-flex align-items-center"
2-
*ngIf="!(isHidden$ | async) && !failed && (remark | dsListMetricProps: 'data-badge-enabled':isListElement == true)">
2+
*ngIf="!(isHidden$ | async) &&
3+
canLoadScript &&
4+
!failed &&
5+
(remark | dsListMetricProps: 'data-badge-enabled':isListElement == true)"
6+
>
37
<div class="col-5 text-left">
48
<div
59
#metricChild
610
class="__dimensions_badge_embed__"
7-
[attr.data-hide-zero-citations]="remark | dsListMetricProps: 'data-hide-zero-citations':isListElement"
11+
[attr.data-hide-zero-citations]="visibleWithoutData ? false : (remark | dsListMetricProps: 'data-hide-zero-citations':isListElement)"
812
[attr.data-pmid]="
913
(remark | dsListMetricProps: 'data-doi':isListElement)
1014
? null
@@ -21,3 +25,9 @@
2125
</div>
2226
</div>
2327
</div>
28+
<div class="row d-flex align-items-center justify-content-center m-2" *ngIf="!canLoadScript && !isListElement">
29+
<div>
30+
{{ "third-party-metrics-cookies.message" | translate: {metricType: metric.metricType | titlecase} }}
31+
<div role="button" class="btn-link" (click)="requestSettingsConsent.emit(true)">{{"third-party-metrics-cookies.consent-settings" | translate}}</div>
32+
</div>
33+
</div>

src/app/shared/metric/metric-loader/base-embedded-metric.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export abstract class BaseEmbeddedMetricComponent extends BaseMetricComponent im
4646
* When the html content has been initialized, initialize the script.
4747
*/
4848
ngAfterViewInit() {
49-
if (this.metric) {
49+
if (this.metric && this.canLoadScript) {
5050
this.initScript();
5151
}
5252
}

src/app/shared/metric/metric-loader/base-metric.component.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,26 @@ export abstract class BaseMetricComponent {
2020

2121
@Output() hide: EventEmitter<boolean> = new EventEmitter();
2222

23+
/**
24+
* Emitter to trigger a new prompt of the cookies modal
25+
*/
26+
@Output() requestSettingsConsent: EventEmitter<boolean> = new EventEmitter();
27+
2328
/**
2429
* A boolean representing if the metric content is hidden or not
2530
*/
2631
isHidden$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
2732

33+
/**
34+
* A boolean to check if the component can load the associated script
35+
*/
36+
canLoadScript = true;
37+
38+
/**
39+
* A boolean to force rendering without data
40+
*/
41+
visibleWithoutData = false;
42+
2843
/**
2944
* Get the detail url form metric remark if present.
3045
*/
Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from '@angular/core';
1+
import { Component, EventEmitter } from '@angular/core';
22
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
33

44
import { of } from 'rxjs';
@@ -8,22 +8,48 @@ import { MetricLoaderService } from './metric-loader.service';
88
import { metric1Mock } from '../../../cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.spec';
99
import { MetricStyleConfigPipe } from '../pipes/metric-style-config/metric-style-config.pipe';
1010
import SpyObj = jasmine.SpyObj;
11+
import { CookieConsents, KlaroService } from '../../cookies/klaro.service';
12+
import { BaseMetricComponent } from './base-metric.component';
13+
14+
1115

1216
describe('MetricLoaderComponent', () => {
1317
let component: MetricLoaderComponent;
1418
let fixture: ComponentFixture<MetricLoaderComponent>;
1519
let metricLoaderService: SpyObj<MetricLoaderService>;
20+
let klaroServiceSpy: jasmine.SpyObj<KlaroService>;
21+
22+
23+
const consentsAccepted: CookieConsents = {
24+
acknowledgement: true,
25+
authentication: true,
26+
preferences: true
27+
};
28+
1629

1730
beforeEach(waitForAsync(() => {
31+
(TestComponent as unknown as BaseMetricComponent).hide = new EventEmitter();
32+
(TestComponent as unknown as BaseMetricComponent).requestSettingsConsent = new EventEmitter();
1833
metricLoaderService = jasmine.createSpyObj('MetricLoaderService', {
1934
loadMetricTypeComponent: jasmine.createSpy('loadMetricTypeComponent')
2035
});
2136
metricLoaderService.loadMetricTypeComponent.and.returnValue(of(TestComponent).toPromise());
2237

38+
klaroServiceSpy = jasmine.createSpyObj('KlaroService', {
39+
getSavedPreferences: jasmine.createSpy('getSavedPreferences'),
40+
watchConsentUpdates: jasmine.createSpy('watchConsentUpdates')
41+
},{
42+
consentsUpdates$: of(consentsAccepted)
43+
});
44+
45+
klaroServiceSpy.getSavedPreferences.and.returnValue(of(consentsAccepted));
46+
47+
2348
TestBed.configureTestingModule({
2449
declarations: [ MetricLoaderComponent, MetricStyleConfigPipe ],
2550
providers: [
26-
{ provide: MetricLoaderService, useValue: metricLoaderService }
51+
{ provide: MetricLoaderService, useValue: metricLoaderService },
52+
{ provide: KlaroService, useValue: klaroServiceSpy },
2753
],
2854
})
2955
.compileComponents();
@@ -32,14 +58,15 @@ describe('MetricLoaderComponent', () => {
3258
beforeEach(() => {
3359
fixture = TestBed.createComponent(MetricLoaderComponent);
3460
component = fixture.componentInstance;
61+
component.metric = metric1Mock;
3562
spyOn(component, 'loadComponent').and.callThrough();
3663

3764
fixture.detectChanges();
3865
});
3966

4067
it('should create', () => {
4168
expect(component).toBeTruthy();
42-
expect(component.loadComponent).toHaveBeenCalledWith(component.metric);
69+
expect(component.loadComponent).toHaveBeenCalledWith(component.metric, true);
4370
});
4471

4572
describe('loadComponent', () => {
@@ -50,16 +77,48 @@ describe('MetricLoaderComponent', () => {
5077

5178
it('should instantiate the component loaded from service', fakeAsync(() => {
5279

53-
component.loadComponent(metric1Mock);
80+
component.loadComponent(metric1Mock, true);
5481
tick(); // wait loadMetricT
5582

56-
expect(metricLoaderService.loadMetricTypeComponent).toHaveBeenCalledWith(metric1Mock.metricType);
57-
expect(component.instantiateComponent).toHaveBeenCalledWith(TestComponent, metric1Mock);
83+
expect(metricLoaderService.loadMetricTypeComponent).toHaveBeenCalledWith(metric1Mock.metricType, true);
84+
expect(component.instantiateComponent).toHaveBeenCalledWith(TestComponent, metric1Mock, true, undefined);
5885

5986
}));
6087

6188
});
6289

90+
describe('Script handling', () => {
91+
92+
beforeEach(() => {
93+
klaroServiceSpy.getSavedPreferences.and.returnValue(of(consentsAccepted));
94+
});
95+
96+
it('should instantiate the component without loading the script', fakeAsync(() => {
97+
98+
component.loadComponent(metric1Mock, false);
99+
tick(); // wait loadMetricT
100+
101+
expect(metricLoaderService.loadMetricTypeComponent).toHaveBeenCalledWith(metric1Mock.metricType, false);
102+
expect(((TestComponent as unknown as BaseMetricComponent).canLoadScript)).toBeFalsy();
103+
}));
104+
105+
});
106+
107+
108+
describe('getCanLoadScript', () => {
109+
110+
it('should return true for not restricted metrics', fakeAsync(() => {
111+
expect((component as any).getCanLoadScript(consentsAccepted)).toBeTruthy();
112+
}));
113+
114+
it('should return false for restricted metrics', fakeAsync(() => {
115+
const consentRejected = {...consentsAccepted, acknowledgement: false};
116+
component.metric = {...metric1Mock, metricType: 'altmetric'};
117+
expect((component as any).getCanLoadScript(consentRejected)).toBeFalsy();
118+
}));
119+
120+
});
121+
63122
});
64123

65124
// declare a test component
@@ -68,5 +127,6 @@ describe('MetricLoaderComponent', () => {
68127
template: ``
69128
})
70129
class TestComponent {
71-
130+
hide = new EventEmitter();
131+
requestSettingsConsent = new EventEmitter();
72132
}

0 commit comments

Comments
 (0)