Skip to content

Commit 01cc3ab

Browse files
authored
Merge pull request #4724 from im-shubham-vish/fix-9814-backport-8x
[Port dspace-8_x] Fix for #8916 #8917 #8918 to 8.x: The Community Administrator should not be able to view all communities/collections in the create/edit community and collection sections (Previous PR #9814 #4639)
2 parents 17f5024 + 7d03792 commit 01cc3ab

20 files changed

+299
-27
lines changed

src/app/core/data/collection-data.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ export class CollectionDataService extends ComColDataService<Collection> {
7777
* requested after the response becomes stale
7878
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
7979
* {@link HALLink}s should be automatically resolved
80+
* @param searchHref The backend search endpoint to use (default to submit)
8081
* @return Observable<RemoteData<PaginatedList<Collection>>>
8182
* collection list
8283
*/
83-
getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
84-
const searchHref = 'findSubmitAuthorized';
84+
getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, searchHref: string = 'findSubmitAuthorized', ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
8585
options = Object.assign({}, options, {
8686
searchParams: [new RequestParam('query', query)],
8787
});

src/app/core/data/community-data.service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import { isNotEmpty } from '../../shared/empty.util';
1111
import { NotificationsService } from '../../shared/notifications/notifications.service';
1212
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
1313
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
14+
import { RequestParam } from '../cache/models/request-param.model';
1415
import { ObjectCacheService } from '../cache/object-cache.service';
1516
import { Community } from '../shared/community.model';
1617
import { HALEndpointService } from '../shared/hal-endpoint.service';
18+
import { getAllCompletedRemoteData } from '../shared/operators';
1719
import { BitstreamDataService } from './bitstream-data.service';
1820
import { ComColDataService } from './comcol-data.service';
1921
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
@@ -38,6 +40,32 @@ export class CommunityDataService extends ComColDataService<Community> {
3840
super('communities', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService);
3941
}
4042

43+
/**
44+
* Get all communities the user is authorized to submit to
45+
*
46+
* @param query limit the returned community to those with metadata values
47+
* matching the query terms.
48+
* @param options The [[FindListOptions]] object
49+
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
50+
* no valid cached version. Defaults to true
51+
* @param reRequestOnStale Whether or not the request should automatically be re-
52+
* requested after the response becomes stale
53+
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
54+
* {@link HALLink}s should be automatically resolved
55+
* @return Observable<RemoteData<PaginatedList<Community>>>
56+
* community list
57+
*/
58+
getAuthorizedCommunity(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Community>[]): Observable<RemoteData<PaginatedList<Community>>> {
59+
const searchHref = 'findAdminAuthorized';
60+
options = Object.assign({}, options, {
61+
searchParams: [new RequestParam('query', query)],
62+
});
63+
64+
return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
65+
getAllCompletedRemoteData(),
66+
);
67+
}
68+
4169
// this method is overridden in order to make it public
4270
getEndpoint() {
4371
return this.halService.getEndpoint(this.linkPath);

src/app/shared/collection-dropdown/collection-dropdown.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
141141
*/
142142
@Input() entityType: string;
143143

144+
/**
145+
* Search endpoint to use for finding authorized collections.
146+
* Defaults to 'findSubmitAuthorized', but can be overridden (e.g. to 'findAdminAuthorized')
147+
*/
148+
@Input() searchHref = 'findSubmitAuthorized';
149+
144150
/**
145151
* Emit to notify whether search is complete
146152
*/
@@ -249,7 +255,7 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
249255
followLink('parentCommunity'));
250256
} else {
251257
searchListService$ = this.collectionDataService
252-
.getAuthorizedCollection(query, findOptions, true, true, followLink('parentCommunity'));
258+
.getAuthorizedCollection(query, findOptions, true, true, this.searchHref, followLink('parentCommunity'));
253259
}
254260
this.searchListCollection$ = searchListService$.pipe(
255261
getFirstCompletedRemoteData(),

src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,19 @@ describe('AuthorizedCollectionSelectorComponent', () => {
8383
});
8484
});
8585
});
86+
87+
describe('when using searchHref', () => {
88+
it('should call getAuthorizedCollection with "findAdminAuthorized" when overridden', (done) => {
89+
component.searchHref = 'findAdminAuthorized';
90+
91+
component.search('', 1).subscribe(() => {
92+
expect(collectionService.getAuthorizedCollection).toHaveBeenCalledWith(
93+
'', jasmine.any(Object), true, false, 'findAdminAuthorized', jasmine.anything(),
94+
);
95+
done();
96+
});
97+
});
98+
});
99+
86100
});
87101
});

src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
5858
*/
5959
@Input() entityType: string;
6060

61+
/**
62+
* Search endpoint to use for finding authorized collections.
63+
* Defaults to 'findSubmitAuthorized', but can be overridden (e.g. to 'findAdminAuthorized')
64+
*/
65+
@Input() searchHref = 'findSubmitAuthorized';
66+
6167
constructor(
6268
protected searchService: SearchService,
6369
protected collectionDataService: CollectionDataService,
@@ -96,7 +102,7 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
96102
findOptions);
97103
} else {
98104
searchListService$ = this.collectionDataService
99-
.getAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
105+
.getAuthorizedCollection(query, findOptions, useCache, false, this.searchHref, followLink('parentCommunity'));
100106
}
101107
return searchListService$.pipe(
102108
getFirstCompletedRemoteData(),
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
waitForAsync,
6+
} from '@angular/core/testing';
7+
import { RouterTestingModule } from '@angular/router/testing';
8+
import { TranslateModule } from '@ngx-translate/core';
9+
10+
import { CommunityDataService } from '../../../../core/data/community-data.service';
11+
import { Community } from '../../../../core/shared/community.model';
12+
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
13+
import { SearchService } from '../../../../core/shared/search/search.service';
14+
import { ThemedLoadingComponent } from '../../../loading/themed-loading.component';
15+
import { NotificationsService } from '../../../notifications/notifications.service';
16+
import { ListableObjectComponentLoaderComponent } from '../../../object-collection/shared/listable-object/listable-object-component-loader.component';
17+
import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
18+
import { createPaginatedList } from '../../../testing/utils.test';
19+
import { VarDirective } from '../../../utils/var.directive';
20+
import { AuthorizedCommunitySelectorComponent } from './authorized-community-selector.component';
21+
22+
describe('AuthorizedCommunitySelectorComponent', () => {
23+
let component: AuthorizedCommunitySelectorComponent;
24+
let fixture: ComponentFixture<AuthorizedCommunitySelectorComponent>;
25+
26+
let communityService;
27+
let community;
28+
29+
let notificationsService: NotificationsService;
30+
31+
beforeEach(waitForAsync(() => {
32+
community = Object.assign(new Community(), {
33+
id: 'authorized-community',
34+
});
35+
communityService = jasmine.createSpyObj('communityService', {
36+
getAuthorizedCommunity: createSuccessfulRemoteDataObject$(createPaginatedList([community])),
37+
});
38+
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
39+
TestBed.configureTestingModule({
40+
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), AuthorizedCommunitySelectorComponent, VarDirective],
41+
providers: [
42+
{ provide: SearchService, useValue: {} },
43+
{ provide: CommunityDataService, useValue: communityService },
44+
{ provide: NotificationsService, useValue: notificationsService },
45+
],
46+
schemas: [NO_ERRORS_SCHEMA],
47+
})
48+
.overrideComponent(AuthorizedCommunitySelectorComponent, {
49+
remove: { imports: [ListableObjectComponentLoaderComponent, ThemedLoadingComponent] },
50+
})
51+
.compileComponents();
52+
}));
53+
54+
beforeEach(() => {
55+
fixture = TestBed.createComponent(AuthorizedCommunitySelectorComponent);
56+
component = fixture.componentInstance;
57+
component.types = [DSpaceObjectType.COMMUNITY];
58+
fixture.detectChanges();
59+
});
60+
61+
describe('search', () => {
62+
it('should call getAuthorizedCommunity and return the authorized community in a SearchResult', (done) => {
63+
component.search('', 1).subscribe((resultRD) => {
64+
expect(communityService.getAuthorizedCommunity).toHaveBeenCalled();
65+
expect(resultRD.payload.page.length).toEqual(1);
66+
expect(resultRD.payload.page[0].indexableObject).toEqual(community);
67+
done();
68+
});
69+
});
70+
});
71+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
AsyncPipe,
3+
CommonModule,
4+
NgClass,
5+
NgIf,
6+
} from '@angular/common';
7+
import { Component } from '@angular/core';
8+
import {
9+
FormsModule,
10+
ReactiveFormsModule,
11+
} from '@angular/forms';
12+
import {
13+
TranslateModule,
14+
TranslateService,
15+
} from '@ngx-translate/core';
16+
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
17+
import { Observable } from 'rxjs';
18+
import { map } from 'rxjs/operators';
19+
20+
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
21+
import { CommunityDataService } from '../../../../core/data/community-data.service';
22+
import { FindListOptions } from '../../../../core/data/find-list-options.model';
23+
import {
24+
buildPaginatedList,
25+
PaginatedList,
26+
} from '../../../../core/data/paginated-list.model';
27+
import { RemoteData } from '../../../../core/data/remote-data';
28+
import { Community } from '../../../../core/shared/community.model';
29+
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
30+
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
31+
import { SearchService } from '../../../../core/shared/search/search.service';
32+
import { hasValue } from '../../../empty.util';
33+
import { HoverClassDirective } from '../../../hover-class.directive';
34+
import { ThemedLoadingComponent } from '../../../loading/themed-loading.component';
35+
import { NotificationsService } from '../../../notifications/notifications.service';
36+
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
37+
import { ListableObjectComponentLoaderComponent } from '../../../object-collection/shared/listable-object/listable-object-component-loader.component';
38+
import { SearchResult } from '../../../search/models/search-result.model';
39+
import { followLink } from '../../../utils/follow-link-config.model';
40+
import { DSOSelectorComponent } from '../dso-selector.component';
41+
42+
@Component({
43+
selector: 'ds-authorized-community-selector',
44+
styleUrls: ['../dso-selector.component.scss'],
45+
templateUrl: '../dso-selector.component.html',
46+
standalone: true,
47+
imports: [
48+
AsyncPipe,
49+
CommonModule,
50+
FormsModule,
51+
HoverClassDirective,
52+
InfiniteScrollModule,
53+
ListableObjectComponentLoaderComponent,
54+
NgClass,
55+
NgIf,
56+
ReactiveFormsModule,
57+
ThemedLoadingComponent,
58+
TranslateModule,
59+
],
60+
})
61+
/**
62+
* Component rendering a list of communities to select from
63+
*/
64+
export class AuthorizedCommunitySelectorComponent extends DSOSelectorComponent {
65+
/**
66+
* If present this value is used to filter community list by entity type
67+
*/
68+
69+
constructor(
70+
protected searchService: SearchService,
71+
protected communityDataService: CommunityDataService,
72+
protected notifcationsService: NotificationsService,
73+
protected translate: TranslateService,
74+
protected dsoNameService: DSONameService,
75+
) {
76+
super(searchService, notifcationsService, translate, dsoNameService);
77+
}
78+
79+
/**
80+
* Get a query to send for retrieving the current DSO
81+
*/
82+
getCurrentDSOQuery(): string {
83+
return this.currentDSOId;
84+
}
85+
86+
/**
87+
* Perform a search for authorized communities with the current query and page
88+
* @param query Query to search objects for
89+
* @param page Page to retrieve
90+
* @param useCache Whether or not to use the cache
91+
*/
92+
search(query: string, page: number, useCache: boolean = true): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
93+
let searchListService$: Observable<RemoteData<PaginatedList<Community>>> = null;
94+
const findOptions: FindListOptions = {
95+
currentPage: page,
96+
elementsPerPage: this.defaultPagination.pageSize,
97+
};
98+
99+
searchListService$ = this.communityDataService
100+
.getAuthorizedCommunity(query, findOptions, useCache, false, followLink('parentCommunity'));
101+
102+
return searchListService$.pipe(
103+
getFirstCompletedRemoteData(),
104+
map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
105+
payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CommunitySearchResult(), { indexableObject: col }))) : null,
106+
})),
107+
);
108+
}
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div>
2+
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
3+
<button type="button" class="btn-close" (click)="close()" aria-label="Close">
4+
</button>
5+
</div>
6+
<div class="modal-body">
7+
<span *ngIf="header" class="h5 px-2">{{header | translate}}</span>
8+
<ds-authorized-community-selector [currentDSOId]="dsoRD?.payload.uuid"
9+
[types]="selectorTypes"
10+
(onSelect)="selectObject($event)"></ds-authorized-community-selector>
11+
</div>
12+
</div>

src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Community } from '../../../../core/shared/community.model';
1818
import { MetadataValue } from '../../../../core/shared/metadata.models';
1919
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
2020
import { RouterStub } from '../../../testing/router.stub';
21-
import { DSOSelectorComponent } from '../../dso-selector/dso-selector.component';
21+
import { AuthorizedCommunitySelectorComponent } from '../../dso-selector/authorized-community-selector/authorized-community-selector.component';
2222
import { CreateCollectionParentSelectorComponent } from './create-collection-parent-selector.component';
2323

2424
describe('CreateCollectionParentSelectorComponent', () => {
@@ -64,7 +64,7 @@ describe('CreateCollectionParentSelectorComponent', () => {
6464
schemas: [NO_ERRORS_SCHEMA],
6565
})
6666
.overrideComponent(CreateCollectionParentSelectorComponent, {
67-
remove: { imports: [DSOSelectorComponent] },
67+
remove: { imports: [AuthorizedCommunitySelectorComponent] },
6868
})
6969
.compileComponents();
7070

src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from '../../../../core/cache/models/sort-options.model';
2323
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
2424
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
25-
import { DSOSelectorComponent } from '../../dso-selector/dso-selector.component';
25+
import { AuthorizedCommunitySelectorComponent } from '../../dso-selector/authorized-community-selector/authorized-community-selector.component';
2626
import {
2727
DSOSelectorModalWrapperComponent,
2828
SelectorActionType,
@@ -34,9 +34,9 @@ import {
3434

3535
@Component({
3636
selector: 'ds-base-create-collection-parent-selector',
37-
templateUrl: '../dso-selector-modal-wrapper.component.html',
37+
templateUrl: './create-collection-parent-selector.component.html',
3838
standalone: true,
39-
imports: [NgIf, DSOSelectorComponent, TranslateModule],
39+
imports: [NgIf, AuthorizedCommunitySelectorComponent, TranslateModule],
4040
})
4141
export class CreateCollectionParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
4242
objectType = DSpaceObjectType.COLLECTION;

0 commit comments

Comments
 (0)