Skip to content

Commit 5aab53e

Browse files
Fix for #8918 The Community Administrator should not be able to view all communities/collections in the create/edit community and collection sections
1 parent 77731af commit 5aab53e

16 files changed

+273
-23
lines changed

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);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
describe('when has no entity type', () => {
63+
it('should call getAuthorizedCommunity and return the authorized community in a SearchResult', (done) => {
64+
component.search('', 1).subscribe((resultRD) => {
65+
expect(communityService.getAuthorizedCommunity).toHaveBeenCalled();
66+
expect(resultRD.payload.page.length).toEqual(1);
67+
expect(resultRD.payload.page[0].indexableObject).toEqual(community);
68+
done();
69+
});
70+
});
71+
});
72+
73+
});
74+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
AsyncPipe,
3+
NgClass,
4+
} from '@angular/common';
5+
import { Component } from '@angular/core';
6+
import {
7+
FormsModule,
8+
ReactiveFormsModule,
9+
} from '@angular/forms';
10+
import {
11+
TranslateModule,
12+
TranslateService,
13+
} from '@ngx-translate/core';
14+
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
15+
import { Observable } from 'rxjs';
16+
import { map } from 'rxjs/operators';
17+
18+
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
19+
import { CommunityDataService } from '../../../../core/data/community-data.service';
20+
import { FindListOptions } from '../../../../core/data/find-list-options.model';
21+
import {
22+
buildPaginatedList,
23+
PaginatedList,
24+
} from '../../../../core/data/paginated-list.model';
25+
import { RemoteData } from '../../../../core/data/remote-data';
26+
import { Community } from '../../../../core/shared/community.model';
27+
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
28+
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
29+
import { SearchService } from '../../../../core/shared/search/search.service';
30+
import { hasValue } from '../../../empty.util';
31+
import { HoverClassDirective } from '../../../hover-class.directive';
32+
import { ThemedLoadingComponent } from '../../../loading/themed-loading.component';
33+
import { NotificationsService } from '../../../notifications/notifications.service';
34+
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
35+
import { ListableObjectComponentLoaderComponent } from '../../../object-collection/shared/listable-object/listable-object-component-loader.component';
36+
import { SearchResult } from '../../../search/models/search-result.model';
37+
import { followLink } from '../../../utils/follow-link-config.model';
38+
import { DSOSelectorComponent } from '../dso-selector.component';
39+
40+
@Component({
41+
selector: 'ds-authorized-community-selector',
42+
styleUrls: ['../dso-selector.component.scss'],
43+
templateUrl: '../dso-selector.component.html',
44+
standalone: true,
45+
imports: [
46+
AsyncPipe,
47+
FormsModule,
48+
HoverClassDirective,
49+
InfiniteScrollModule,
50+
ListableObjectComponentLoaderComponent,
51+
NgClass,
52+
ReactiveFormsModule,
53+
ThemedLoadingComponent,
54+
TranslateModule,
55+
],
56+
})
57+
/**
58+
* Component rendering a list of communities to select from
59+
*/
60+
export class AuthorizedCommunitySelectorComponent extends DSOSelectorComponent {
61+
/**
62+
* If present this value is used to filter community list by entity type
63+
*/
64+
65+
constructor(
66+
protected searchService: SearchService,
67+
protected communityDataService: CommunityDataService,
68+
protected notifcationsService: NotificationsService,
69+
protected translate: TranslateService,
70+
protected dsoNameService: DSONameService,
71+
) {
72+
super(searchService, notifcationsService, translate, dsoNameService);
73+
}
74+
75+
/**
76+
* Get a query to send for retrieving the current DSO
77+
*/
78+
getCurrentDSOQuery(): string {
79+
return this.currentDSOId;
80+
}
81+
82+
/**
83+
* Perform a search for authorized communities with the current query and page
84+
* @param query Query to search objects for
85+
* @param page Page to retrieve
86+
* @param useCache Whether or not to use the cache
87+
*/
88+
search(query: string, page: number, useCache: boolean = true): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
89+
let searchListService$: Observable<RemoteData<PaginatedList<Community>>> = null;
90+
const findOptions: FindListOptions = {
91+
currentPage: page,
92+
elementsPerPage: this.defaultPagination.pageSize,
93+
};
94+
95+
searchListService$ = this.communityDataService
96+
.getAuthorizedCommunity(query, findOptions, useCache, false, followLink('parentCommunity'));
97+
98+
return searchListService$.pipe(
99+
getFirstCompletedRemoteData(),
100+
map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
101+
payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CommunitySearchResult(), { indexableObject: col }))) : null,
102+
})),
103+
);
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
@if (header) {
8+
<span class="h5 px-2">{{header | translate}}</span>
9+
}
10+
<ds-authorized-community-selector [currentDSOId]="dsoRD?.payload.uuid"
11+
[types]="selectorTypes"
12+
(onSelect)="selectObject($event)"></ds-authorized-community-selector>
13+
</div>
14+
</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,10 +34,10 @@ 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,
3939
imports: [
40-
DSOSelectorComponent,
40+
AuthorizedCommunitySelectorComponent,
4141
TranslateModule,
4242
],
4343
})

src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
</div>
1717
}
1818
<span class="h5 px-2">{{'dso-selector.create.community.sub-level' | translate}}</span>
19-
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" [sort]="defaultSort" (onSelect)="selectObject($event)"></ds-dso-selector>
19+
<ds-authorized-community-selector [currentDSOId]="dsoRD?.payload.uuid"
20+
[types]="selectorTypes"
21+
(onSelect)="selectObject($event)"></ds-authorized-community-selector>
2022
</div>
2123
</div>
2224

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

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

2727
describe('CreateCommunityParentSelectorComponent', () => {
@@ -69,7 +69,7 @@ describe('CreateCommunityParentSelectorComponent', () => {
6969
schemas: [NO_ERRORS_SCHEMA],
7070
})
7171
.overrideComponent(CreateCommunityParentSelectorComponent, {
72-
remove: { imports: [DSOSelectorComponent] },
72+
remove: { imports: [AuthorizedCommunitySelectorComponent] },
7373
})
7474
.compileComponents();
7575

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i
2626
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
2727
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
2828
import { hasValue } from '../../../empty.util';
29-
import { DSOSelectorComponent } from '../../dso-selector/dso-selector.component';
29+
import { AuthorizedCommunitySelectorComponent } from '../../dso-selector/authorized-community-selector/authorized-community-selector.component';
3030
import {
3131
DSOSelectorModalWrapperComponent,
3232
SelectorActionType,
@@ -46,7 +46,7 @@ import {
4646
standalone: true,
4747
imports: [
4848
AsyncPipe,
49-
DSOSelectorComponent,
49+
AuthorizedCommunitySelectorComponent,
5050
TranslateModule,
5151
],
5252
})
@@ -62,7 +62,6 @@ export class CreateCommunityParentSelectorComponent extends DSOSelectorModalWrap
6262
}
6363

6464
ngOnInit() {
65-
super.ngOnInit();
6665
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
6766
}
6867

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
@if (header) {
8+
<span class="h5 px-2">{{header | translate}}</span>
9+
}
10+
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
11+
[types]="selectorTypes"
12+
(onSelect)="selectObject($event)"></ds-authorized-collection-selector>
13+
</div>
14+
</div>

0 commit comments

Comments
 (0)