Skip to content

Commit aaaae17

Browse files
FrancescoMolinaroAndrea Barbasso
authored andcommitted
Merged in task/dspace-cris-2025_02_x/DSC-1956 (pull request DSpace#4635)
Task/dspace cris 2025 02 x/DSC-1956 Approved-by: Andrea Barbasso
2 parents 93fd862 + 7efe616 commit aaaae17

43 files changed

Lines changed: 1320 additions & 46 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cypress/e2e/community-create.cy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ beforeEach(() => {
44
});
55

66
it('should show loading component while saving', () => {
7-
cy.intercept('**/sites/**canSubmit**').as('canSubmit');
8-
cy.wait('@canSubmit');
7+
cy.intercept('**/api/config/properties/submit.type-bind.field').as('typeBind');
8+
cy.wait('@typeBind');
99

1010
const title = 'Test Community Title';
1111
cy.get('#title').type(title);

src/app/app.effects.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NavbarEffects } from './navbar/navbar.effects';
2+
import { AuthorizationEffects } from './shared/authorizations/authorization.effects';
23
import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects';
34
import { NotificationsEffects } from './shared/notifications/notifications.effects';
45
import { SidebarEffects } from './shared/sidebar/sidebar-effects.service';
@@ -12,4 +13,5 @@ export const appEffects = [
1213
SidebarEffects,
1314
ThemeEffects,
1415
RelationshipEffects,
16+
AuthorizationEffects,
1517
];

src/app/app.reducer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
editItemRelationshipsReducer,
3434
EditItemRelationshipsState,
3535
} from './edit-item-relationships/edit-item-relationships.reducer';
36+
import { AuthorizationsState } from './shared/authorizations/authorization.interfaces';
37+
import { authorizationReducer } from './shared/authorizations/authorization.reducer';
3638
import {
3739
contextHelpReducer,
3840
ContextHelpState,
@@ -107,6 +109,7 @@ export interface AppState {
107109
correlationId: string;
108110
contextHelp: ContextHelpState;
109111
editItemRelationships: EditItemRelationshipsState;
112+
authorizationFeatures: AuthorizationsState;
110113
}
111114

112115
export const appReducers: ActionReducerMap<AppState> = {
@@ -131,6 +134,7 @@ export const appReducers: ActionReducerMap<AppState> = {
131134
contextHelp: contextHelpReducer,
132135
statistics: StatisticsReducer,
133136
editItemRelationships: editItemRelationshipsReducer,
137+
authorizationFeatures: authorizationReducer,
134138
};
135139

136140
export const routerStateSelector = (state: AppState) => state.router;

src/app/bitstream-page/bitstream-page-authorizations.guard.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
} from 'rxjs';
1010
import { AuthService } from 'src/app/core/auth/auth.service';
1111
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
12-
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
1312

1413
import { BitstreamDataService } from '../core/data/bitstream-data.service';
14+
import { FeatureID } from '../core/data/feature-authorization/feature-id';
1515
import { Bitstream } from '../core/shared/bitstream.model';
1616
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
1717
import { bitstreamPageAuthorizationsGuard } from './bitstream-page-authorizations.guard';

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

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
1-
import { Injectable } from '@angular/core';
1+
import {
2+
Inject,
3+
Injectable,
4+
} from '@angular/core';
25
import isArray from 'lodash/isArray';
36
import {
7+
combineLatest,
48
Observable,
59
of,
610
} from 'rxjs';
711
import {
12+
filter,
813
map,
914
switchMap,
15+
take,
1016
} from 'rxjs/operators';
17+
import { SearchOptions } from 'src/app/shared/search/models/search-options.model';
1118

19+
import {
20+
APP_CONFIG,
21+
AppConfig,
22+
} from '../../../config/app-config.interface';
1223
import { FollowAuthorityMetadata } from '../../../config/search-follow-metadata.interface';
1324
import { environment } from '../../../environments/environment';
25+
import { AuthorizationService } from '../../shared/authorizations/authorization.service';
1426
import {
1527
hasValue,
1628
isNotEmpty,
1729
} from '../../shared/empty.util';
1830
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
1931
import { SearchObjects } from '../../shared/search/models/search-objects.model';
2032
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
33+
import { getRequestIdFromParams } from '../data/feature-authorization/authorization-utils';
34+
import { FeatureID } from '../data/feature-authorization/feature-id';
2135
import { ItemDataService } from '../data/item-data.service';
2236
import { PaginatedList } from '../data/paginated-list.model';
2337
import { RemoteData } from '../data/remote-data';
@@ -43,6 +57,8 @@ export class SearchManager {
4357
protected itemService: ItemDataService,
4458
protected browseService: BrowseService,
4559
protected searchService: SearchService,
60+
protected authorizationService: AuthorizationService,
61+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
4662
) {
4763
}
4864

@@ -79,7 +95,7 @@ export class SearchManager {
7995
...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<SearchObjects<T>>> {
8096
const optionsWithDefaultProjection = Object.assign(new PaginatedSearchOptions({}), searchOptions, { projection: searchOptions.projection ?? 'preventMetadataSecurity' });
8197
return this.searchService.search(optionsWithDefaultProjection, responseMsToLive, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)
82-
.pipe(this.completeSearchObjectsWithExtraData());
98+
.pipe(this.completeSearchObjectsWithExtraData(optionsWithDefaultProjection));
8399
}
84100

85101

@@ -94,19 +110,111 @@ export class SearchManager {
94110
});
95111
}
96112

97-
protected completeSearchObjectsWithExtraData<T extends DSpaceObject>() {
113+
protected completeSearchObjectsWithExtraData<T extends DSpaceObject>(searchOptions: SearchOptions) {
98114
return switchMap((searchObjectsRD: RemoteData<SearchObjects<T>>) => {
99115
if (searchObjectsRD.isSuccess) {
100116
const items: Item[] = searchObjectsRD.payload.page
101117
.map((searchResult) => isNotEmpty(searchResult?._embedded?.indexableObject) ? searchResult._embedded.indexableObject : searchResult.indexableObject) as any;
102-
return this.fetchExtraData(items).pipe(map(() => {
103-
return searchObjectsRD;
104-
}));
118+
return this.fetchExtraData(items).pipe(
119+
switchMap(() => this.fetchConfiguredAuthorizations(searchObjectsRD, searchOptions.configuration ?? 'default')),
120+
map(() => {
121+
return searchObjectsRD;
122+
}),
123+
);
105124
}
106125
return of(searchObjectsRD);
107126
});
108127
}
109128

129+
/**
130+
* Retrieve configured authorizations related to current discovery configuration
131+
*
132+
* @param searchObjects
133+
* @param configuration
134+
* @protected
135+
*/
136+
protected fetchConfiguredAuthorizations<T extends DSpaceObject>(searchObjects: RemoteData<SearchObjects<T>>, configuration: string): Observable<any> {
137+
const objects = searchObjects.payload.page.map((searchResult) => searchResult.indexableObject) as any;
138+
const mappedObjects = this.getConfiguredAuthorizationsMap(objects, configuration);
139+
140+
if ([...mappedObjects.keys()].length === 0) {
141+
return of(searchObjects);
142+
}
143+
144+
const requestsIds = [];
145+
146+
const uiidListsMappedToAuthorizations = this.groupItemsUuidsByAuthorizations(objects, mappedObjects);
147+
[...uiidListsMappedToAuthorizations.keys()].forEach((features) => {
148+
const uuidList = uiidListsMappedToAuthorizations.get(features);
149+
const type = objects.find(object => object.id === uuidList[0]).uniqueType;
150+
const hrefs = objects.map(dso => dso.self);
151+
this.authorizationService.initStateForObjects(uuidList, type, features, hrefs);
152+
requestsIds.push(getRequestIdFromParams(type, uuidList, features));
153+
});
154+
155+
return combineLatest(requestsIds.map(id => this.authorizationService.isRequestLoading(id))).pipe(
156+
filter(loadingItems => loadingItems.every(loading => !loading)),
157+
take(1),
158+
map(() => {
159+
return searchObjects;
160+
}),
161+
);
162+
}
163+
164+
/**
165+
* Group items by authorization ID in a map
166+
*
167+
* @param objects
168+
* @param mappedEntities
169+
* @private
170+
*/
171+
private groupItemsUuidsByAuthorizations<T extends DSpaceObject>(objects: T[], mappedEntities: Map<string, FeatureID[]>): Map<FeatureID[], string[]> {
172+
const mappedUuidListsToFeatures = new Map();
173+
174+
objects.forEach(object => {
175+
const objectType = object.uniqueType;
176+
const features = mappedEntities.get(objectType);
177+
178+
if (hasValue(features) && hasValue(mappedUuidListsToFeatures.get(features))) {
179+
mappedUuidListsToFeatures.set(features, [...mappedUuidListsToFeatures.get(features), object.id]);
180+
} else if (hasValue(features)) {
181+
mappedUuidListsToFeatures.set(features, [object.id]);
182+
}
183+
});
184+
185+
return mappedUuidListsToFeatures;
186+
}
187+
188+
/**
189+
* Map entity types oe unique type to required authorizations so that we can group the items by feature
190+
*
191+
* @param objects
192+
* @param configuration
193+
* @private
194+
*/
195+
private getConfiguredAuthorizationsMap<T extends DSpaceObject>(objects: T[], configuration: string): Map<string, FeatureID[]> {
196+
const configuredAuthorizationsForDiscovery =
197+
this.appConfig.discoveryAuthorizationFeaturesConfig[configuration] ?? this.appConfig.discoveryAuthorizationFeaturesConfig.default;
198+
const configuredAuthorizationsToType = new Map();
199+
200+
if (!hasValue(configuredAuthorizationsForDiscovery)) {
201+
return configuredAuthorizationsToType;
202+
}
203+
204+
const objectUniqueTypes = [...new Set(objects.map(dso => dso?.uniqueType))];
205+
206+
objectUniqueTypes.forEach((entity) => {
207+
const config = configuredAuthorizationsForDiscovery[entity];
208+
209+
if (hasValue(config)) {
210+
configuredAuthorizationsToType.set(entity, config);
211+
}
212+
});
213+
214+
return configuredAuthorizationsToType;
215+
}
216+
217+
110218
protected fetchExtraData<T extends DSpaceObject>(objects: T[]): Observable<any> {
111219

112220
const items: Item[] = objects

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { of } from 'rxjs';
33
import { TestScheduler } from 'rxjs/testing';
44
import { v4 as uuidv4 } from 'uuid';
55

6+
import { AppConfig } from '../../../config/app-config.interface';
67
import { toRemoteData } from '../../browse-by/browse-by-metadata/browse-by-metadata.component.spec';
78
import {
89
createSuccessfulRemoteDataObject,
910
createSuccessfulRemoteDataObject$,
1011
} from '../../shared/remote-data.utils';
1112
import { createPaginatedList } from '../../shared/testing/utils.test';
1213
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
14+
import { FeatureID } from '../data/feature-authorization/feature-id';
1315
import { FindListOptions } from '../data/find-list-options.model';
1416
import { Item } from '../shared/item.model';
1517
import { ITEM } from '../shared/item.resource-type';
@@ -97,6 +99,20 @@ describe('SearchManager', () => {
9799
type: ITEM.value,
98100
});
99101

102+
const appConfig: Partial<AppConfig> = {
103+
discoveryAuthorizationFeaturesConfig: {},
104+
followAuthorityMetadata: [
105+
{
106+
type: 'Publication',
107+
metadata: ['dc.contributor.author'],
108+
},
109+
{
110+
type: 'Product',
111+
metadata: ['dc.contributor.author'],
112+
},
113+
],
114+
};
115+
100116
const mockBrowseService: any = {
101117
getBrowseItemsFor: (options: BrowseEntrySearchOptions) =>
102118
toRemoteData([firstPublication, secondPublication, firstProject]),
@@ -114,8 +130,13 @@ describe('SearchManager', () => {
114130
of(createSuccessfulRemoteDataObject(createPaginatedList([]))),
115131
};
116132

133+
const mockAuthorizationService: any = {
134+
getObjectsAuthorizations: (uuidList: string[], uniqueType: string, featuresId?: FeatureID[]) =>
135+
of([]),
136+
};
137+
117138
function initTestService() {
118-
return new SearchManager(mockItemService, mockBrowseService, mockSearchService);
139+
return new SearchManager(mockItemService, mockBrowseService, mockSearchService, mockAuthorizationService, appConfig as AppConfig);
119140
}
120141

121142
beforeEach(() => {

0 commit comments

Comments
 (0)