Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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