Skip to content

Commit 175ed1b

Browse files
committed
Merge branch 'dspace-cris-2023_02_x' into ux-plus-2023_02_x
# Conflicts: # src/app/shared/browse-most-elements/abstract-browse-elements.component.ts
2 parents 4626bac + 02e29eb commit 175ed1b

38 files changed

Lines changed: 443 additions & 83 deletions

File tree

bitbucket-pipelines.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pipelines:
2222
branches:
2323
'ux-plus-2023_02_x':
2424
- step: *unittest-code-checks
25+
'prod/**':
26+
- step: *unittest-code-checks
2527
pull-requests:
2628
'**':
2729
- step: *unittest-code-checks

config/config.example.yml

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,6 @@ item:
327327
# The maximum number of values for repeatable metadata to show in the full item
328328
metadataLimit: 20
329329

330-
# When the search results are retrieved, for each item type the metadata with a valid authority value are inspected.
331-
# Referenced items will be fetched with a find all by id strategy to avoid individual rest requests
332-
# to efficiently display the search results.
333-
followAuthorityMetadata:
334-
- type: Publication
335-
metadata: dc.contributor.author
336-
- type: Product
337-
metadata: dc.contributor.author
338-
339330
# Collection Page Config
340331
collection:
341332
edit:
@@ -533,3 +524,19 @@ addToAnyPlugin:
533524
title: DSpace CRIS 7 demo
534525
# The link to be shown in the shared post, if different from document.location.origin (optional)
535526
# link: https://dspacecris7.4science.cloud/
527+
528+
# When the search results are retrieved, for each item type the metadata with a valid authority value are inspected.
529+
# Referenced items will be fetched with a find all by id strategy to avoid individual rest requests
530+
# to efficiently display the search results.
531+
followAuthorityMetadata:
532+
- type: Publication
533+
metadata: dc.contributor.author
534+
- type: Product
535+
metadata: dc.contributor.author
536+
537+
# The maximum number of item to process when following authority metadata values.
538+
followAuthorityMaxItemLimit: 100
539+
540+
# The maximum number of metadata values to process for each metadata key
541+
# when following authority metadata values.
542+
followAuthorityMetadataValuesLimit: 5

src/app/collection-page/collection-page.component.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import { RouterStub } from '../shared/testing/router.stub';
1414
import { environment } from 'src/environments/environment.test';
1515
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
1616
import { Collection } from '../core/shared/collection.model';
17-
import { SearchService } from '../core/shared/search/search.service';
1817
import { By } from '@angular/platform-browser';
1918
import { FormsModule } from '@angular/forms';
2019
import { RouterTestingModule } from '@angular/router/testing';
2120
import { TranslateModule } from '@ngx-translate/core';
2221
import { VarDirective } from '../shared/utils/var.directive';
2322
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
2423
import { Bitstream } from '../core/shared/bitstream.model';
24+
import { SearchManager } from '../core/browse/search-manager';
2525

2626
describe('CollectionPageComponent', () => {
2727
let component: CollectionPageComponent;
@@ -33,7 +33,7 @@ describe('CollectionPageComponent', () => {
3333
let paginationServiceSpy: jasmine.SpyObj<PaginationService>;
3434
let authorizationDataServiceSpy: jasmine.SpyObj<AuthorizationDataService>;
3535
let dsoNameServiceSpy: jasmine.SpyObj<DSONameService>;
36-
let searchServiceSpy: jasmine.SpyObj<SearchService>;
36+
let searchServiceSpy: jasmine.SpyObj<SearchManager>;
3737
let aroute = new ActivatedRouteStub();
3838
let router = new RouterStub();
3939

@@ -44,7 +44,7 @@ describe('CollectionPageComponent', () => {
4444
paginationServiceSpy = jasmine.createSpyObj('PaginationService', ['getCurrentPagination', 'getCurrentSort', 'clearPagination']);
4545
authorizationDataServiceSpy = jasmine.createSpyObj('AuthorizationDataService', ['isAuthorized']);
4646
collectionDataServiceSpy = jasmine.createSpyObj('CollectionDataService', ['findById', 'getAuthorizedCollection']);
47-
searchServiceSpy = jasmine.createSpyObj('SearchService', ['search']);
47+
searchServiceSpy = jasmine.createSpyObj('SearchManager', ['search']);
4848
dsoNameServiceSpy = jasmine.createSpyObj('DSONameService', ['getName']);
4949

5050
await TestBed.configureTestingModule({
@@ -58,7 +58,7 @@ describe('CollectionPageComponent', () => {
5858
{ provide: PaginationService, useValue: paginationServiceSpy },
5959
{ provide: AuthorizationDataService, useValue: authorizationDataServiceSpy },
6060
{ provide: DSONameService, useValue: dsoNameServiceSpy },
61-
{ provide: SearchService, useValue: searchServiceSpy },
61+
{ provide: SearchManager, useValue: searchServiceSpy },
6262
{ provide: APP_CONFIG, useValue: environment },
6363
{ provide: PLATFORM_ID, useValue: 'browser' },
6464
]

src/app/collection-page/collection-page.component.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
44
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
55
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
66
import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model';
7-
import { SearchService } from '../core/shared/search/search.service';
87
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
9-
import { CollectionDataService } from '../core/data/collection-data.service';
108
import { PaginatedList } from '../core/data/paginated-list.model';
119
import { RemoteData } from '../core/data/remote-data';
1210
import { Bitstream } from '../core/shared/bitstream.model';
@@ -30,6 +28,7 @@ import { redirectOn4xx } from '../core/shared/authorized.operators';
3028
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
3129
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
3230
import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface';
31+
import { SearchManager } from '../core/browse/search-manager';
3332

3433
@Component({
3534
selector: 'ds-collection-page',
@@ -64,8 +63,7 @@ export class CollectionPageComponent implements OnInit {
6463

6564
constructor(
6665
@Inject(PLATFORM_ID) private platformId: Object,
67-
private collectionDataService: CollectionDataService,
68-
private searchService: SearchService,
66+
private searchManager: SearchManager,
6967
private route: ActivatedRoute,
7068
private router: Router,
7169
private authService: AuthService,
@@ -113,14 +111,13 @@ export class CollectionPageComponent implements OnInit {
113111
getFirstSucceededRemoteData(),
114112
map((rd) => rd.payload.id),
115113
switchMap((id: string) => {
116-
return this.searchService.search<Item>(
114+
return this.searchManager.search<Item>(
117115
new PaginatedSearchOptions({
118116
scope: id,
119117
pagination: currentPagination,
120118
sort: currentSort,
121119
dsoTypes: [DSpaceObjectType.ITEM],
122120
forcedEmbeddedKeys: ['metrics'],
123-
projection: 'preventMetadataSecurity'
124121
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW)
125122
.pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
126123
}),

src/app/core/browse/search-manager.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class SearchManager {
4545
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
4646
*/
4747
getBrowseItemsFor(filterValue: string, filterAuthority: string, options: BrowseEntrySearchOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<PaginatedList<Item>>> {
48-
const browseOptions = Object.assign({}, options, { projection: 'preventMetadataSecurity' });
48+
const browseOptions = Object.assign({}, options, { projection: options.projection ?? 'preventMetadataSecurity' });
4949
return this.browseService.getBrowseItemsFor(filterValue, filterAuthority, browseOptions, ...linksToFollow)
5050
.pipe(this.completeWithExtraData());
5151
}
@@ -67,7 +67,8 @@ export class SearchManager {
6767
useCachedVersionIfAvailable = true,
6868
reRequestOnStale = true,
6969
...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<SearchObjects<T>>> {
70-
return this.searchService.search(searchOptions, responseMsToLive, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)
70+
const optionsWithDefaultProjection = Object.assign(new PaginatedSearchOptions({}), searchOptions, { projection: searchOptions.projection ?? 'preventMetadataSecurity' });
71+
return this.searchService.search(optionsWithDefaultProjection, responseMsToLive, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)
7172
.pipe(this.completeSearchObjectsWithExtraData());
7273
}
7374

@@ -112,7 +113,8 @@ export class SearchManager {
112113
})
113114
.filter((item) => hasValue(item));
114115

115-
const uuidList = this.extractUUID(items, environment.followAuthorityMetadata);
116+
const uuidList = this.extractUUID(items, environment.followAuthorityMetadata, environment.followAuthorityMaxItemLimit);
117+
116118
return uuidList.length > 0 ? this.itemService.findAllById(uuidList).pipe(
117119
getFirstCompletedRemoteData(),
118120
map(data => {
@@ -125,27 +127,31 @@ export class SearchManager {
125127
) : of(null);
126128
}
127129

128-
protected extractUUID(items: Item[], metadataToFollow: FollowAuthorityMetadata[]): string[] {
130+
protected extractUUID(items: Item[], metadataToFollow: FollowAuthorityMetadata[], numberOfElementsToReturn?: number): string[] {
129131
const uuidMap = {};
130132

131133
items.forEach((item) => {
132134
metadataToFollow.forEach((followMetadata: FollowAuthorityMetadata) => {
133135
if (item.entityType === followMetadata.type) {
134136
if (isArray(followMetadata.metadata)) {
135-
followMetadata.metadata.forEach((metadata) => {
136-
Metadata.all(item.metadata, metadata)
137+
followMetadata.metadata.forEach((metadata) => {
138+
Metadata.all(item.metadata, metadata, null, environment.followAuthorityMetadataValuesLimit)
137139
.filter((metadataValue: MetadataValue) => Metadata.hasValidItemAuthority(metadataValue.authority))
138140
.forEach((metadataValue: MetadataValue) => uuidMap[metadataValue.authority] = metadataValue);
139141
});
140142
} else {
141-
Metadata.all(item.metadata, followMetadata.metadata)
143+
Metadata.all(item.metadata, followMetadata.metadata, null, environment.followAuthorityMetadataValuesLimit)
142144
.filter((metadataValue: MetadataValue) => Metadata.hasValidItemAuthority(metadataValue.authority))
143145
.forEach((metadataValue: MetadataValue) => uuidMap[metadataValue.authority] = metadataValue);
144146
}
145147
}
146148
});
147149
});
148150

151+
if (hasValue(numberOfElementsToReturn) && numberOfElementsToReturn > 0) {
152+
return Object.keys(uuidMap).slice(0, numberOfElementsToReturn);
153+
}
154+
149155
return Object.keys(uuidMap);
150156
}
151157
}

src/app/core/browse/search.manager.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ describe('SearchManager', () => {
182182
const uuidList = (service as any).extractUUID([firstPublication, firstPublication], [{type: 'Publication', metadata: ['dc.contributor.author']}]);
183183
expect(uuidList).toEqual([validAuthority]);
184184
});
185+
186+
it('should limit the number of extracted uuids', () => {
187+
const uuidList = (service as any).extractUUID([firstPublication, secondPublication, invalidAuthorityPublication], [{type: 'Publication', metadata: ['dc.contributor.author']}], 2);
188+
expect(uuidList.length).toBe(2);
189+
});
185190
});
186191

187192
});

src/app/core/shared/dspace-object.model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
115115
return Metadata.all(this.metadata, keyOrKeys, valueFilter);
116116
}
117117

118+
/**
119+
* Gets all matching metadata in this DSpaceObject, up to a limit.
120+
*
121+
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
122+
* @param {number} limit The maximum number of results to return.
123+
* @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done.
124+
* @returns {MetadataValue[]} the matching values or an empty array.
125+
*/
126+
limitedMetadata(keyOrKeys: string | string[], limit: number, valueFilter?: MetadataValueFilter): MetadataValue[] {
127+
return Metadata.all(this.metadata, keyOrKeys, valueFilter, limit);
128+
}
129+
118130
/**
119131
* Like [[allMetadata]], but only returns string values.
120132
*

src/app/core/shared/metadata.utils.spec.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isUndefined } from '../../shared/empty.util';
22
import { v4 as uuidv4 } from 'uuid';
33
import { MetadataMap, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models';
4-
import { Metadata } from './metadata.utils';
4+
import { Metadata, PLACEHOLDER_VALUE } from './metadata.utils';
55

66
const mdValue = (value: string, language?: string, authority?: string): MetadataValue => {
77
return Object.assign(new MetadataValue(), {
@@ -44,19 +44,20 @@ const multiViewModelList = [
4444
{ key: 'foo', ...bar, order: 0 }
4545
];
4646

47-
const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => {
47+
const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?, limit?: number) => {
4848
const keys = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys];
4949
describe('and key' + (keys.length === 1 ? (' ' + keys[0]) : ('s ' + JSON.stringify(keys)))
5050
+ ' with ' + (isUndefined(filter) ? 'no filter' : 'filter ' + JSON.stringify(filter)), () => {
51-
const result = fn(mapOrMaps, keys, filter);
51+
const result = fn(mapOrMaps, keys, filter, limit);
5252
let shouldReturn;
5353
if (resultKind === 'boolean') {
5454
shouldReturn = expected;
5555
} else if (isUndefined(expected)) {
5656
shouldReturn = 'undefined';
5757
} else if (expected instanceof Array) {
5858
shouldReturn = 'an array with ' + expected.length + ' ' + (expected.length > 1 ? 'ordered ' : '')
59-
+ resultKind + (expected.length !== 1 ? 's' : '');
59+
+ resultKind + (expected.length !== 1 ? 's' : '')
60+
+ (isUndefined(limit) ? '' : ' (limited to ' + limit + ')');
6061
} else {
6162
shouldReturn = 'a ' + resultKind;
6263
}
@@ -297,4 +298,29 @@ describe('Metadata', () => {
297298

298299
});
299300

301+
describe('all method with limit', () => {
302+
const testAllWithLimit = (mapOrMaps, keyOrKeys, expected, limit) =>
303+
testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, expected, undefined, limit);
304+
305+
describe('with multiMap and limit', () => {
306+
testAllWithLimit(multiMap, 'dc.title', [dcTitle1], 1);
307+
});
308+
});
309+
310+
describe('Placeholder values', () => {
311+
it('should ignore placeholder values in get methods', () => {
312+
const placeholderMd = mdValue(PLACEHOLDER_VALUE);
313+
const key = 'dc.test.placeholder';
314+
const map = { 'dc.test.placeholder': [placeholderMd] };
315+
316+
expect(Metadata.all(map, key).length).toEqual(0);
317+
expect(Metadata.allValues(map, key).length).toEqual(0);
318+
expect(Metadata.has(map, key)).toBeFalsy();
319+
expect(Metadata.first(map, key)).toBeUndefined();
320+
expect(Metadata.firstValue(map, key)).toBeUndefined();
321+
expect(Metadata.hasValue(placeholderMd)).toBeFalsy();
322+
expect(Metadata.valueMatches(placeholderMd, null)).toBeFalsy();
323+
});
324+
});
325+
300326
});

src/app/core/shared/metadata.utils.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { validate as uuidValidate } from 'uuid';
1414

1515
export const AUTHORITY_GENERATE = 'will be generated::';
1616
export const AUTHORITY_REFERENCE = 'will be referenced::';
17+
export const PLACEHOLDER_VALUE = '#PLACEHOLDER_PARENT_METADATA_VALUE#';
18+
1719

1820
/**
1921
* Utility class for working with DSpace object metadata.
@@ -29,18 +31,18 @@ export const AUTHORITY_REFERENCE = 'will be referenced::';
2931
* followed by any other (non-dc) metadata values.
3032
*/
3133
export class Metadata {
32-
3334
/**
3435
* Gets all matching metadata in the map(s).
3536
*
3637
* @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). When multiple maps are given, they will be
3738
* checked in order, and only values from the first with at least one match will be returned.
3839
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
3940
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
41+
* @param {number} limit The maximum number of values to return. If unspecified, all matching values will be returned.
4042
* @returns {MetadataValue[]} the matching values or an empty array.
4143
*/
4244
public static all(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
43-
filter?: MetadataValueFilter): MetadataValue[] {
45+
filter?: MetadataValueFilter, limit?: number): MetadataValue[] {
4446
const mdMaps: MetadataMapInterface[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps];
4547
const matches: MetadataValue[] = [];
4648
for (const mdMap of mdMaps) {
@@ -50,6 +52,9 @@ export class Metadata {
5052
for (const candidate of candidates) {
5153
if (Metadata.valueMatches(candidate as MetadataValue, filter)) {
5254
matches.push(candidate as MetadataValue);
55+
if (hasValue(limit) && matches.length >= limit) {
56+
return matches;
57+
}
5358
}
5459
}
5560
}
@@ -148,11 +153,11 @@ export class Metadata {
148153
* Returns true if this Metadatum's value is defined
149154
*/
150155
public static hasValue(value: MetadataValue|string): boolean {
151-
if (isEmpty(value)) {
156+
if (isEmpty(value) || value === PLACEHOLDER_VALUE) {
152157
return false;
153158
}
154159
if (isObject(value) && value.hasOwnProperty('value')) {
155-
return isNotEmpty(value.value);
160+
return isNotEmpty(value.value) && value.value !== PLACEHOLDER_VALUE;
156161
}
157162
return true;
158163
}
@@ -165,7 +170,9 @@ export class Metadata {
165170
* @returns {boolean} whether the filter matches, or true if no filter is given.
166171
*/
167172
public static valueMatches(mdValue: MetadataValue, filter: MetadataValueFilter) {
168-
if (!filter) {
173+
if (mdValue.value === PLACEHOLDER_VALUE) {
174+
return false;
175+
} else if (!filter) {
169176
return true;
170177
} else if (filter.language && filter.language !== mdValue.language) {
171178
return false;

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/crisref/crisref.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('CrisrefComponent', () => {
2020
let fixture: ComponentFixture<CrisrefComponent>;
2121

2222
const itemService = jasmine.createSpyObj('ItemDataService', {
23-
findById: jasmine.createSpy('findById')
23+
findByIdWithProjections: jasmine.createSpy('findByIdWithProjections')
2424
});
2525
const metadataValue = Object.assign(new MetadataValue(), {
2626
'value': 'test item title',
@@ -106,7 +106,7 @@ describe('CrisrefComponent', () => {
106106
beforeEach(() => {
107107
fixture = TestBed.createComponent(CrisrefComponent);
108108
component = fixture.componentInstance;
109-
itemService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testPerson));
109+
itemService.findByIdWithProjections.and.returnValue(createSuccessfulRemoteDataObject$(testPerson));
110110
fixture.detectChanges();
111111
});
112112

0 commit comments

Comments
 (0)