Skip to content
Merged
7 changes: 6 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@
"aot": true,
"assets": [
"src/assets",
"src/robots.txt"
"src/robots.txt",
{
"glob": "favicon.ico",
"input": "src/themes/datashare/assets/images",
"output": "/"
}
],
"styles": [
"src/styles/startup.scss",
Expand Down
4 changes: 4 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,10 @@ info:
enableEndUserAgreement: true
enablePrivacyStatement: true
enableCOARNotifySupport: true
# Only enable Google Analytics on installations that configure a `google.analytics.key`
# property on the backend. When false (default) the UI does not probe for that property,
# avoiding a 404 request on every page.
enableGoogleAnalytics: false

# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
# display in supported metadata fields. By default, only dc.description.abstract is supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export class DatashareSubmissionFormSectionContainerService {
* @param id The ID of the panel to open, or null to close all panels
*/
setOpenPanelId(id: string | null): void {
console.log('Setting open panel ID to:', id);
this.openPanelId.set(id);
}
}
2 changes: 0 additions & 2 deletions src/app/datashare/datashare-submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ export class DatashareSubmissionService {
constructor(private notificationsService: NotificationsService,
private translate: TranslateService,
) {
console.log('DatashareSubmissionService created');
}

/**
* Update the deposit button visibility state
*/
updatehasUploadFilesErrors(show: boolean): void {
console.log('Updating hasUploadFilesErrors to:', show);
this._hasUploadFilesErrorsSignal.set(show);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,6 @@ export class FileSectionComponent implements OnInit {
this.notificationsService.error(this.translateService.get('file-section.error.header'), `${bitstreamsRD.statusCode} ${bitstreamsRD.errorMessage}`);
} else if (hasValue(bitstreamsRD.payload)) {
const current: Bitstream[] = this.bitstreams$.getValue();
// For debugging.
bitstreamsRD.payload.page.forEach(bitstream => {
console.log('Bitstream:', bitstream);
});
this.bitstreams$.next([...current, ...bitstreamsRD.payload.page]);
this.isLoading = false;
this.isLastPage = this.currentPage === bitstreamsRD.payload.totalPages;
Expand All @@ -169,10 +165,6 @@ export class FileSectionComponent implements OnInit {
this.notificationsService.error(this.translateService.get('file-section.error.header'), `${licenseRD.statusCode} ${licenseRD.errorMessage}`);
} else if (hasValue(licenseRD.payload)) {
const updated: Bitstream[] = this.bitstreams$.getValue();
// For debugging.
licenseRD.payload.page.forEach(bitstream => {
console.log('Bitstream:', bitstream);
});
this.bitstreams$.next([...updated, ...licenseRD.payload.page]);
}
this.isLoading = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SplitPipe } from 'src/app/shared/utils/split.pipe';
import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { RequestService } from '../../../core/data/request.service';
import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model';
import { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service';
Expand All @@ -28,6 +29,7 @@ describe('QaEventNotificationComponent', () => {
let component: QaEventNotificationComponent;
let fixture: ComponentFixture<QaEventNotificationComponent>;
let qualityAssuranceSourceDataServiceStub: any;
let authorizationServiceStub: any;

const obj = Object.assign(new QualityAssuranceSourceObject(), {
id: 'sourceName:target',
Expand All @@ -41,14 +43,18 @@ describe('QaEventNotificationComponent', () => {
beforeEach(async () => {

qualityAssuranceSourceDataServiceStub = {
getSourcesByTarget: () => objPL,
getSourcesByTarget: jasmine.createSpy('getSourcesByTarget').and.returnValue(objPL),
};
authorizationServiceStub = {
isAuthorized: jasmine.createSpy('isAuthorized').and.returnValue(of(true)),
};
await TestBed.configureTestingModule({
imports: [CommonModule, TranslateModule.forRoot(), QaEventNotificationComponent, SplitPipe],
providers: [
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub },
{ provide: AuthorizationDataService, useValue: authorizationServiceStub },
{ provide: RequestService, useValue: {} },
{ provide: NotificationsService, useValue: {} },
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('test') },
Expand All @@ -57,6 +63,12 @@ describe('QaEventNotificationComponent', () => {
provideMockStore({}),
],
})
// The component declares QualityAssuranceSourceDataService in its own `providers`,
// which would otherwise shadow the stub above with a real instance. Override it so
// the component uses the stub we can assert against.
.overrideComponent(QaEventNotificationComponent, {
set: { providers: [{ provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }] },
})
.compileComponents();
fixture = TestBed.createComponent(QaEventNotificationComponent);
component = fixture.componentInstance;
Expand All @@ -78,4 +90,22 @@ describe('QaEventNotificationComponent', () => {
const route = component.getQualityAssuranceRoute();
expect(route).toBe('/notifications/quality-assurance');
});

it('should request QA sources when the user is authorized to see QA', () => {
authorizationServiceStub.isAuthorized.and.returnValue(of(true));
qualityAssuranceSourceDataServiceStub.getSourcesByTarget.calls.reset();
let result: QualityAssuranceSourceObject[];
component.getQualityAssuranceSources$().subscribe((sources) => result = sources);
expect(qualityAssuranceSourceDataServiceStub.getSourcesByTarget).toHaveBeenCalled();
expect(result).toEqual([obj]);
});

it('should NOT request QA sources when the user is not authorized', () => {
authorizationServiceStub.isAuthorized.and.returnValue(of(false));
qualityAssuranceSourceDataServiceStub.getSourcesByTarget.calls.reset();
let result: QualityAssuranceSourceObject[];
component.getQualityAssuranceSources$().subscribe((sources) => result = sources);
expect(qualityAssuranceSourceDataServiceStub.getSourcesByTarget).not.toHaveBeenCalled();
expect(result).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ import {
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import {
Observable,
of as observableOf,
} from 'rxjs';
import {
catchError,
map,
switchMap,
} from 'rxjs/operators';

import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths';
import { RequestParam } from '../../../core/cache/models/request-param.model';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
Expand Down Expand Up @@ -61,6 +67,7 @@ export class QaEventNotificationComponent implements OnChanges {

constructor(
private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService,
private authorizationService: AuthorizationDataService,
) {}

/**
Expand All @@ -77,20 +84,31 @@ export class QaEventNotificationComponent implements OnChanges {
* Note: sourceId is composed as: id: "sourceName:<target>"
*/
getQualityAssuranceSources$(): Observable<QualityAssuranceSourceObject[]> {
const findListTopicOptions: FindListOptions = {
searchParams: [new RequestParam('target', this.item.uuid)],
};
return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false)
.pipe(
getFirstCompletedRemoteData(),
map((data: RemoteData<PaginatedList<QualityAssuranceSourceObject>>) => {
if (data.hasSucceeded) {
return data.payload.page;
}
return [];
}),
catchError(() => []),
);
// Quality Assurance sources are only available to authorized users. Checking the
// authorization first avoids issuing the `qualityassurancesources/search/byTarget`
// request for anonymous/unauthorized users, which would otherwise return a 401 that
// shows up as a failed request in the browser. Authorized users keep the same behavior.
return this.authorizationService.isAuthorized(FeatureID.CanSeeQA).pipe(
switchMap((canSeeQA: boolean) => {
if (!canSeeQA) {
return observableOf([] as QualityAssuranceSourceObject[]);
}
const findListTopicOptions: FindListOptions = {
searchParams: [new RequestParam('target', this.item.uuid)],
};
return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false)
.pipe(
getFirstCompletedRemoteData(),
map((data: RemoteData<PaginatedList<QualityAssuranceSourceObject>>) => {
if (data.hasSucceeded) {
return data.payload.page;
}
return [];
}),
);
}),
catchError(() => observableOf([] as QualityAssuranceSourceObject[])),
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,6 @@ export class AccessControlFormContainerComponent<T extends DSpaceObject> impleme
* Will be used from a parent component to read the value of the form
*/
getFormValue() {
console.log({
bitstream: this.bitstreamAccessCmp.getValue(),
item: this.itemAccessCmp.getValue(),
state: this.state,
});
return {
bitstream: this.bitstreamAccessCmp.getValue(),
item: this.itemAccessCmp.getValue(),
Expand Down Expand Up @@ -137,9 +132,7 @@ export class AccessControlFormContainerComponent<T extends DSpaceObject> impleme
this.bulkAccessControlService.executeScript(
[ this.itemRD.payload.uuid ],
file,
).pipe(take(1)).subscribe((res) => {
console.log('success', res);
});
).pipe(take(1)).subscribe();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ export class BulkAccessControlService {
* @param file
*/
executeScript(uuids: string[], file: File): Observable<boolean> {
console.log('execute', { uuids, file });

const params: ProcessParameter[] = [
{ name: '-f', value: file.name },
];
Expand Down
22 changes: 22 additions & 0 deletions src/app/shared/cookies/browser-klaro.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';

import { environment } from '../../../environments/environment';
import { AuthService } from '../../core/auth/auth.service';
import { RestResponse } from '../../core/cache/response.models';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
Expand Down Expand Up @@ -310,6 +311,9 @@ describe('BrowserKlaroService', () => {
let GOOGLE_ANALYTICS_KEY;
let REGISTRATION_VERIFICATION_ENABLED_KEY;
beforeEach(() => {
// The Google Analytics key is only probed when GA is enabled for the installation;
// these tests exercise that probe-and-filter path.
environment.info.enableGoogleAnalytics = true;
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
Expand All @@ -320,6 +324,10 @@ describe('BrowserKlaroService', () => {
configurationDataService.findByPropertyName = findByPropertyName;
});

afterEach(() => {
environment.info.enableGoogleAnalytics = false;
});

it('should not filter googleAnalytics when servicesToHide are empty', () => {
const filteredConfig = (service as any).filterConfigServices([]);
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
Expand Down Expand Up @@ -401,5 +409,19 @@ describe('BrowserKlaroService', () => {
service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
it('should hide googleAnalytics without requesting the key when GA is disabled', () => {
environment.info.enableGoogleAnalytics = false;
const findByPropertyNameSpy = jasmine.createSpy('findByPropertyName').and.returnValue(
createSuccessfulRemoteDataObject$({
...new ConfigurationProperty(),
name: trackingIdTestValue,
values: ['false'],
}),
);
configurationDataService.findByPropertyName = findByPropertyNameSpy;
service.initialize();
expect(findByPropertyNameSpy).not.toHaveBeenCalledWith(GOOGLE_ANALYTICS_KEY);
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
});
});
});
13 changes: 9 additions & 4 deletions src/app/shared/cookies/browser-klaro.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,15 @@ export class BrowserKlaroService extends KlaroService {
this.klaroConfig.translations.zy.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
}

const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
getFirstCompletedRemoteData(),
map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
);
// Only probe the backend for the Google Analytics key when GA is enabled for this
// installation. When it is disabled we hide the GA service from the consent UI without
// making a request that would otherwise 404 (the `google.analytics.key` property is unset).
const hideGoogleAnalytics$ = environment.info?.enableGoogleAnalytics
? this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
getFirstCompletedRemoteData(),
map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
)
: observableOf(true);

const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe(
getFirstCompletedRemoteData(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,27 @@ describe('SearchExportCsvComponent', () => {
});
});
});
describe('bulkedit.export.max.items probe', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));

it('should NOT probe bulkedit.export.max.items when the export button is not shown', () => {
// Non-admin: the button (and therefore the export-limit warning) is never rendered,
// so the config property must not be requested - this is what avoids the per-page 404.
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
(configurationDataService.findByPropertyName as jasmine.Spy).calls.reset();
initBeforeEach();
expect(configurationDataService.findByPropertyName).not.toHaveBeenCalledWith('bulkedit.export.max.items');
});

it('should probe bulkedit.export.max.items once the export button is shown', () => {
// Admin with the export script available: the warning is evaluated, so the limit is fetched.
(configurationDataService.findByPropertyName as jasmine.Spy).calls.reset();
initBeforeEach();
expect(configurationDataService.findByPropertyName).toHaveBeenCalledWith('bulkedit.export.max.items');
});
});
describe('export', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Observable } from 'rxjs';
import {
filter,
map,
shareReplay,
startWith,
switchMap,
} from 'rxjs/operators';
Expand Down Expand Up @@ -92,16 +93,34 @@ export class SearchExportCsvComponent implements OnInit, OnChanges {
switchMap(() => this.scriptDataService.scriptWithNameExistsAndCanExecute('metadata-export-search')),
map((canExecute: boolean) => canExecute),
startWith(false),
shareReplay({ bufferSize: 1, refCount: true }),
);
this.shouldShowWarning$ = this.itemExceeds();
this.shouldShowWarning$ = this.buildShouldShowWarning$();
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.total) {
this.shouldShowWarning$ = this.itemExceeds();
// ngOnChanges runs before ngOnInit, so shouldShowButton$ may not exist on the first
// change; in that case ngOnInit builds the warning observable with the current total.
if (changes.total && hasValue(this.shouldShowButton$)) {
this.shouldShowWarning$ = this.buildShouldShowWarning$();
}
}

/**
* Build the observable that decides whether to show the export-limit warning.
* The backend `bulkedit.export.max.items` property is only requested once the export
* button is shown (i.e. the user is an administrator who can run the export script), so
* anonymous and non-admin users never trigger that request (which would otherwise 404
* when the property is unset).
*/
private buildShouldShowWarning$(): Observable<boolean> {
return this.shouldShowButton$.pipe(
filter((canShow: boolean) => canShow),
switchMap(() => this.itemExceeds()),
startWith(false),
);
}

/**
* Checks if the export limit has been exceeded and updates the tooltip accordingly
*/
Expand Down
Loading
Loading