Merge pull request #645 from 4Science/#601-resource-policies

#601 resource policies
This commit is contained in:
Tim Donohue
2020-05-29 10:03:10 -05:00
committed by GitHub
69 changed files with 4994 additions and 297 deletions

View File

@@ -6,7 +6,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getAccessControlModulePath } from '../admin-routing.module'; import { getAccessControlModulePath } from '../admin-routing.module';
const GROUP_EDIT_PATH = 'groups'; export const GROUP_EDIT_PATH = 'groups';
export function getGroupEditPath(id: string) { export function getGroupEditPath(id: string) {
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString(); return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();

View File

@@ -8,7 +8,7 @@ import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.ser
import { URLCombiner } from '../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
const REGISTRIES_MODULE_PATH = 'registries'; const REGISTRIES_MODULE_PATH = 'registries';
const ACCESS_CONTROL_MODULE_PATH = 'access-control'; export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
export function getRegistriesModulePath() { export function getRegistriesModulePath() {
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString(); return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();

View File

@@ -29,6 +29,9 @@ import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edi
import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component'; import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component';
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
/** /**
* Module that contains all components related to the Edit Item page administrator functionality * Module that contains all components related to the Edit Item page administrator functionality
@@ -67,6 +70,9 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
ItemMoveComponent, ItemMoveComponent,
ItemEditBitstreamDragHandleComponent, ItemEditBitstreamDragHandleComponent,
VirtualMetadataComponent, VirtualMetadataComponent,
ItemAuthorizationsComponent,
ResourcePolicyEditComponent,
ResourcePolicyCreateComponent,
], ],
providers: [ providers: [
BundleDataService BundleDataService

View File

@@ -14,6 +14,12 @@ import { ItemMoveComponent } from './item-move/item-move.component';
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver';
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
@@ -21,6 +27,7 @@ export const ITEM_EDIT_PRIVATE_PATH = 'private';
export const ITEM_EDIT_PUBLIC_PATH = 'public'; export const ITEM_EDIT_PUBLIC_PATH = 'public';
export const ITEM_EDIT_DELETE_PATH = 'delete'; export const ITEM_EDIT_DELETE_PATH = 'delete';
export const ITEM_EDIT_MOVE_PATH = 'move'; export const ITEM_EDIT_MOVE_PATH = 'move';
export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations';
/** /**
* Routing module that handles the routing for the Edit Item page administrator functionality * Routing module that handles the routing for the Edit Item page administrator functionality
@@ -111,12 +118,43 @@ export const ITEM_EDIT_MOVE_PATH = 'move';
path: ITEM_EDIT_MOVE_PATH, path: ITEM_EDIT_MOVE_PATH,
component: ItemMoveComponent, component: ItemMoveComponent,
data: { title: 'item.edit.move.title' }, data: { title: 'item.edit.move.title' },
},
{
path: ITEM_EDIT_AUTHORIZATIONS_PATH,
children: [
{
path: 'create',
resolve: {
resourcePolicyTarget: ResourcePolicyTargetResolver
},
component: ResourcePolicyCreateComponent,
data: { title: 'resource-policies.create.page.title' }
},
{
path: 'edit',
resolve: {
resourcePolicy: ResourcePolicyResolver
},
component: ResourcePolicyEditComponent,
data: { title: 'resource-policies.edit.page.title' }
},
{
path: '',
component: ItemAuthorizationsComponent,
data: { title: 'item.edit.authorizations.title' }
}
]
} }
] ]
} }
]) ])
], ],
providers: [] providers: [
I18nBreadcrumbResolver,
I18nBreadcrumbsService,
ResourcePolicyResolver,
ResourcePolicyTargetResolver
]
}) })
export class EditItemPageRoutingModule { export class EditItemPageRoutingModule {

View File

@@ -0,0 +1,13 @@
<div class="container">
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
<ds-resource-policies [resourceType]="'item'" [resourceUUID]="(getItemUUID() | async)"></ds-resource-policies>
<ng-container *ngFor="let bundle of (getItemBundles() | async); trackById">
<ds-resource-policies [resourceType]="'bundle'"
[resourceUUID]="bundle.id"></ds-resource-policies>
<ng-container *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id) | async)?.page; trackById">
<ds-resource-policies [resourceType]="'bitstream'"
[resourceUUID]="bitstream.id"></ds-resource-policies>
</ng-container>
</ng-container>
</div>

View File

@@ -0,0 +1,183 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { ItemAuthorizationsComponent } from './item-authorizations.component';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { Bundle } from '../../../core/shared/bundle.model';
import { createMockRDPaginatedObs } from '../item-bitstreams/item-bitstreams.component.spec';
import { Item } from '../../../core/shared/item.model';
import { LinkService } from '../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../shared/mocks/link-service.mock';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { createTestComponent } from '../../../shared/testing/utils.test';
import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model';
describe('ItemAuthorizationsComponent test suite', () => {
let comp: ItemAuthorizationsComponent;
let compAsAny: any;
let fixture: ComponentFixture<ItemAuthorizationsComponent>;
let de;
const linkService: any = getMockLinkService();
const bitstream1 = Object.assign(new Bitstream(), {
id: 'bitstream1',
uuid: 'bitstream1'
});
const bitstream2 = Object.assign(new Bitstream(), {
id: 'bitstream2',
uuid: 'bitstream2'
});
const bitstream3 = Object.assign(new Bitstream(), {
id: 'bitstream3',
uuid: 'bitstream3'
});
const bitstream4 = Object.assign(new Bitstream(), {
id: 'bitstream4',
uuid: 'bitstream4'
});
const bundle1 = Object.assign(new Bundle(), {
id: 'bundle1',
uuid: 'bundle1',
_links: {
self: { href: 'bundle1-selflink' }
},
bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2])
});
const bundle2 = Object.assign(new Bundle(), {
id: 'bundle2',
uuid: 'bundle2',
_links: {
self: { href: 'bundle2-selflink' }
},
bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4])
});
const bundles = [bundle1, bundle2];
const bitstreamList1: PaginatedList<Bitstream> = new PaginatedList(new PageInfo(), [bitstream1, bitstream2]);
const bitstreamList2: PaginatedList<Bitstream> = new PaginatedList(new PageInfo(), [bitstream3, bitstream4]);
const item = Object.assign(new Item(), {
uuid: 'item',
id: 'item',
_links: {
self: { href: 'item-selflink' }
},
bundles: createMockRDPaginatedObs([bundle1, bundle2])
});
const routeStub = {
data: observableOf({
item: createSuccessfulRemoteDataObject(item)
})
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot()
],
declarations: [
ItemAuthorizationsComponent,
TestComponent
],
providers: [
{ provide: LinkService, useValue: linkService },
{ provide: ActivatedRoute, useValue: routeStub },
ItemAuthorizationsComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-item-authorizations></ds-item-authorizations>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create ItemAuthorizationsComponent', inject([ItemAuthorizationsComponent], (app: ItemAuthorizationsComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ItemAuthorizationsComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
linkService.resolveLink.and.callFake((object, link) => object);
fixture.detectChanges();
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should init bundles and bitstreams map properly', () => {
expect(compAsAny.subs.length).toBe(2);
expect(compAsAny.bundles$.value).toEqual(bundles);
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
expect(bitstreamList).toBeObservable(cold('(a|)', {
a: bitstreamList1
}));
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
expect(bitstreamList).toBeObservable(cold('(a|)', {
a: bitstreamList2
}));
});
it('should get the item UUID', () => {
expect(comp.getItemUUID()).toBeObservable(cold('(a|)', {
a: item.id
}));
});
it('should get the item\'s bundle', () => {
expect(comp.getItemBundles()).toBeObservable(cold('a', {
a: bundles
}));
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
}

View File

@@ -0,0 +1,155 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list';
import {
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteDataWithNotEmptyPayload
} from '../../../core/shared/operators';
import { Item } from '../../../core/shared/item.model';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { LinkService } from '../../../core/cache/builders/link.service';
import { Bundle } from '../../../core/shared/bundle.model';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { FindListOptions } from '../../../core/data/request.models';
/**
* Interface for a bundle's bitstream map entry
*/
interface BundleBitstreamsMapEntry {
id: string;
bitstreams: Observable<PaginatedList<Bitstream>>
}
@Component({
selector: 'ds-item-authorizations',
templateUrl: './item-authorizations.component.html'
})
/**
* Component that handles the item Authorizations
*/
export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
/**
* A map that contains all bitstream of the item's bundles
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
*/
public bundleBitstreamsMap: Map<string, Observable<PaginatedList<Bitstream>>> = new Map<string, Observable<PaginatedList<Bitstream>>>();
/**
* The list of bundle for the item
* @type {Observable<PaginatedList<Bundle>>}
*/
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
/**
* The target editing item
* @type {Observable<Item>}
*/
private item$: Observable<Item>;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {LinkService} linkService
* @param {ActivatedRoute} route
*/
constructor(
private linkService: LinkService,
private route: ActivatedRoute
) {
}
/**
* Initialize the component, setting up the bundle and bitstream within the item
*/
ngOnInit(): void {
this.item$ = this.route.data.pipe(
map((data) => data.item),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((item: Item) => this.linkService.resolveLink(
item,
followLink('bundles', new FindListOptions(), true, followLink('bitstreams'))
))
) as Observable<Item>;
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
filter((item: Item) => isNotEmpty(item.bundles)),
flatMap((item: Item) => item.bundles),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
catchError((error) => {
console.error(error);
return observableOf(new PaginatedList(null, []))
})
);
this.subs.push(
bundles$.pipe(
take(1),
map((list: PaginatedList<Bundle>) => list.page)
).subscribe((bundles: Bundle[]) => {
this.bundles$.next(bundles);
}),
bundles$.pipe(
take(1),
flatMap((list: PaginatedList<Bundle>) => list.page),
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
).subscribe((entry: BundleBitstreamsMapEntry) => {
this.bundleBitstreamsMap.set(entry.id, entry.bitstreams)
})
)
}
/**
* Return the item's UUID
*/
getItemUUID(): Observable<string> {
return this.item$.pipe(
map((item: Item) => item.id),
first((UUID: string) => isNotEmpty(UUID))
)
}
/**
* Return all item's bundles
*
* @return an observable that emits all item's bundles
*/
getItemBundles(): Observable<Bundle[]> {
return this.bundles$.asObservable();
}
/**
* Return all bundle's bitstreams
*
* @return an observable that emits all item's bundles
*/
private getBundleBitstreams(bundle: Bundle): Observable<PaginatedList<Bitstream>> {
return bundle.bitstreams.pipe(
getFirstSucceededRemoteDataPayload(),
catchError((error) => {
console.error(error);
return observableOf(new PaginatedList(null, []))
})
)
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())
}
}

View File

@@ -68,6 +68,7 @@ export class ItemStatusComponent implements OnInit {
The value is supposed to be a href for the button The value is supposed to be a href for the button
*/ */
this.operations = []; this.operations = [];
this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper')); this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
if (item.isWithdrawn) { if (item.isWithdrawn) {
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate')); this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));

View File

@@ -7,6 +7,7 @@ import { Item } from '../core/shared/item.model';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { find } from 'rxjs/operators'; import { find } from 'rxjs/operators';
import { followLink } from '../shared/utils/follow-link-config.model'; import { followLink } from '../shared/utils/follow-link-config.model';
import { FindListOptions } from '../core/data/request.models';
/** /**
* This class represents a resolver that requests a specific item before the route is activated * This class represents a resolver that requests a specific item before the route is activated
@@ -26,7 +27,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id, return this.itemService.findById(route.params.id,
followLink('owningCollection'), followLink('owningCollection'),
followLink('bundles'), followLink('bundles', new FindListOptions(), true, followLink('bitstreams')),
followLink('relationships'), followLink('relationships'),
followLink('version', undefined, true, followLink('versionhistory')), followLink('version', undefined, true, followLink('versionhistory')),
).pipe( ).pipe(

View File

@@ -33,7 +33,7 @@ export function getBitstreamModulePath() {
return `/${BITSTREAM_MODULE_PATH}`; return `/${BITSTREAM_MODULE_PATH}`;
} }
const ADMIN_MODULE_PATH = 'admin'; export const ADMIN_MODULE_PATH = 'admin';
export function getAdminModulePath() { export function getAdminModulePath() {
return `/${ADMIN_MODULE_PATH}`; return `/${ADMIN_MODULE_PATH}`;

View File

@@ -28,7 +28,8 @@ export class DSONameService {
return dso.firstMetadataValue('organization.legalName'); return dso.firstMetadataValue('organization.legalName');
}, },
Default: (dso: DSpaceObject): string => { Default: (dso: DSpaceObject): string => {
return dso.firstMetadataValue('dc.title'); // If object doesn't have dc.title metadata use name property
return dso.firstMetadataValue('dc.title') || dso.name;
} }
}; };

View File

@@ -2,7 +2,7 @@
/** /**
* Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object * Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object
*/ */
export class SearchParam { export class RequestParam {
constructor(public fieldName: string, public fieldValue: any) { constructor(public fieldName: string, public fieldValue: any) {
} }

View File

@@ -78,7 +78,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipTypeService } from './data/relationship-type.service';
import { RelationshipService } from './data/relationship.service'; import { RelationshipService } from './data/relationship.service';
import { ResourcePolicyService } from './data/resource-policy.service'; import { ResourcePolicyService } from './resource-policy/resource-policy.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service'; import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { SiteDataService } from './data/site-data.service'; import { SiteDataService } from './data/site-data.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
@@ -116,7 +116,7 @@ import { RelationshipType } from './shared/item-relationships/relationship-type.
import { Relationship } from './shared/item-relationships/relationship.model'; import { Relationship } from './shared/item-relationships/relationship.model';
import { Item } from './shared/item.model'; import { Item } from './shared/item.model';
import { License } from './shared/license.model'; import { License } from './shared/license.model';
import { ResourcePolicy } from './shared/resource-policy.model'; import { ResourcePolicy } from './resource-policy/models/resource-policy.model';
import { SearchConfigurationService } from './shared/search/search-configuration.service'; import { SearchConfigurationService } from './shared/search/search-configuration.service';
import { SearchFilterService } from './shared/search/search-filter.service'; import { SearchFilterService } from './shared/search/search-filter.service';
import { SearchService } from './shared/search/search.service'; import { SearchService } from './shared/search/search.service';

View File

@@ -12,7 +12,7 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -94,7 +94,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> { getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findAuthorizedByCommunity'; const searchHref = 'findAuthorizedByCommunity';
options = Object.assign({}, options, { options = Object.assign({}, options, {
searchParams: [new SearchParam('uuid', communityId)] searchParams: [new RequestParam('uuid', communityId)]
}); });
return this.searchBy(searchHref, options).pipe( return this.searchBy(searchHref, options).pipe(

View File

@@ -20,7 +20,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { getClassForType } from '../cache/builders/build-decorators'; import { getClassForType } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ErrorResponse, RestResponse } from '../cache/response.models'; import { ErrorResponse, RestResponse } from '../cache/response.models';
@@ -111,7 +111,7 @@ export abstract class DataService<T extends CacheableObject> {
result$ = this.getSearchEndpoint(searchMethod); result$ = this.getSearchEndpoint(searchMethod);
if (hasValue(options.searchParams)) { if (hasValue(options.searchParams)) {
options.searchParams.forEach((param: SearchParam) => { options.searchParams.forEach((param: RequestParam) => {
args.push(`${param.fieldName}=${param.fieldValue}`); args.push(`${param.fieldName}=${param.fieldValue}`);
}) })
} }
@@ -153,6 +153,33 @@ export abstract class DataService<T extends CacheableObject> {
} }
} }
/**
* Turn an array of RequestParam into a query string and combine it with the given HREF
*
* @param href The HREF to which the query string should be appended
* @param params Array with additional params to combine with query string
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
let args = [];
if (hasValue(params)) {
params.forEach((param: RequestParam) => {
args.push(`${param.fieldName}=${param.fieldValue}`);
})
}
args = this.addEmbedParams(args, ...linksToFollow);
if (isNotEmpty(args)) {
return new URLCombiner(href, `?${args.join('&')}`).toString();
} else {
return href;
}
}
/** /**
* Adds the embed options to the link for the request * Adds the embed options to the link for the request
* @param args params for the query string * @param args params for the query string
@@ -293,9 +320,9 @@ export abstract class DataService<T extends CacheableObject> {
* @param searchMethod The search method for the object * @param searchMethod The search method for the object
*/ */
protected getSearchEndpoint(searchMethod: string): Observable<string> { protected getSearchEndpoint(searchMethod: string): Observable<string> {
return this.halService.getEndpoint(`${this.linkPath}/search`).pipe( return this.halService.getEndpoint(this.linkPath).pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
map((href: string) => `${href}/${searchMethod}`)); map((href: string) => `${href}/search/${searchMethod}`));
} }
/** /**
@@ -380,15 +407,15 @@ export abstract class DataService<T extends CacheableObject> {
* *
* @param {DSpaceObject} dso * @param {DSpaceObject} dso
* The object to create * The object to create
* @param {string} parentUUID * @param {RequestParam[]} params
* The UUID of the parent to create the new object under * Array with additional params to combine with query string
*/ */
create(dso: T, parentUUID: string): Observable<RemoteData<T>> { create(dso: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
isNotEmptyOperator(), isNotEmptyOperator(),
distinctUntilChanged(), distinctUntilChanged(),
map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
); );
const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso); const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso);
@@ -479,7 +506,7 @@ export abstract class DataService<T extends CacheableObject> {
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
return this.requestService.getByUUID(requestId).pipe( return this.requestService.getByUUID(requestId).pipe(
find((request: RequestEntry) => request.completed), find((request: RequestEntry) => isNotEmpty(request) && request.completed),
map((request: RequestEntry) => request.response.isSuccessful) map((request: RequestEntry) => request.response.isSuccessful)
); );
} }

View File

@@ -21,7 +21,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -257,7 +257,7 @@ export class RelationshipService extends DataService<Relationship> {
if (options) { if (options) {
findListOptions = Object.assign(new FindListOptions(), options); findListOptions = Object.assign(new FindListOptions(), options);
} }
const searchParams = [new SearchParam('label', label), new SearchParam('dso', item.id)]; const searchParams = [new RequestParam('label', label), new RequestParam('dso', item.id)];
if (findListOptions.searchParams) { if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else { } else {

View File

@@ -11,7 +11,7 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service'; import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service'; import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
import { MetadataschemaParsingService } from './metadataschema-parsing.service'; import { MetadataschemaParsingService } from './metadataschema-parsing.service';
@@ -146,7 +146,7 @@ export class FindListOptions {
elementsPerPage?: number; elementsPerPage?: number;
currentPage?: number; currentPage?: number;
sort?: SortOptions; sort?: SortOptions;
searchParams?: SearchParam[]; searchParams?: RequestParam[];
startsWith?: string; startsWith?: string;
} }

View File

@@ -1,75 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResourcePolicy } from '../shared/resource-policy.model';
import { RequestService } from './request.service';
import { ResourcePolicyService } from './resource-policy.service';
describe('ResourcePolicyService', () => {
let scheduler: TestScheduler;
let service: ResourcePolicyService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
const testObject = {
uuid: '664184ee-b254-45e8-970d-220e5ccc060b'
} as ResourcePolicy;
const requestURL = `https://rest.api/rest/api/resourcepolicies/${testObject.uuid}`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
beforeEach(() => {
scheduler = getTestScheduler();
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
configure: true
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: cold('a', {
a: {
payload: testObject
}
})
});
objectCache = {} as ObjectCacheService;
const halService = {} as HALEndpointService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
service = new ResourcePolicyService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
http,
comparator
);
spyOn((service as any).dataService, 'findByHref').and.callThrough();
});
describe('findByHref', () => {
it('should proxy the call to dataservice.findByHref', () => {
scheduler.schedule(() => service.findByHref(requestURL));
scheduler.flush();
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL);
});
it('should return a RemoteData<ResourcePolicy> for the object with the given URL', () => {
const result = service.findByHref(requestURL);
const expected = cold('a', {
a: {
payload: testObject
}
});
expect(result).toBeObservable(expected);
});
});
});

View File

@@ -1,95 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { FindListOptions } from '../data/request.models';
import { Collection } from '../shared/collection.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResourcePolicy } from '../shared/resource-policy.model';
import { RemoteData } from '../data/remote-data';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RESOURCE_POLICY } from '../shared/resource-policy.resource-type';
import { ChangeAnalyzer } from './change-analyzer';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { PaginatedList } from './paginated-list';
/* tslint:disable:max-classes-per-file */
/**
* A private DataService implementation to delegate specific methods to.
*/
class DataServiceImpl extends DataService<ResourcePolicy> {
protected linkPath = 'resourcepolicies';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: ChangeAnalyzer<ResourcePolicy>) {
super();
}
}
/**
* A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint
*/
@Injectable()
@dataService(RESOURCE_POLICY)
export class ResourcePolicyService {
private dataService: DataServiceImpl;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ResourcePolicy>) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
}
/**
* Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link ResourcePolicy}
* @param href The url of {@link ResourcePolicy} we want to retrieve
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<ResourcePolicy>> {
return this.dataService.findByHref(href, ...linksToFollow);
}
/**
* Returns a list of observables of {@link RemoteData} of {@link ResourcePolicy}s, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link ResourcePolicy}
* @param href The url of the {@link ResourcePolicy} we want to retrieve
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow);
}
/**
* Return the defaultAccessConditions {@link ResourcePolicy} list for a given {@link Collection}
*
* @param collection the {@link Collection} to retrieve the defaultAccessConditions for
* @param findListOptions the {@link FindListOptions} for the request
*/
getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions);
}
}

View File

@@ -9,7 +9,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ChangeAnalyzer } from '../data/change-analyzer'; import { ChangeAnalyzer } from '../data/change-analyzer';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
@@ -105,7 +105,7 @@ describe('EPersonDataService', () => {
it('search by default scope (byMetadata) and no query', () => { it('search by default scope (byMetadata) and no query', () => {
service.searchByScope(null, ''); service.searchByScope(null, '');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', ''))] searchParams: [Object.assign(new RequestParam('query', ''))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
}); });
@@ -113,7 +113,7 @@ describe('EPersonDataService', () => {
it('search metadata scope and no query', () => { it('search metadata scope and no query', () => {
service.searchByScope('metadata', ''); service.searchByScope('metadata', '');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', ''))] searchParams: [Object.assign(new RequestParam('query', ''))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
}); });
@@ -121,7 +121,7 @@ describe('EPersonDataService', () => {
it('search metadata scope and with query', () => { it('search metadata scope and with query', () => {
service.searchByScope('metadata', 'test'); service.searchByScope('metadata', 'test');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', 'test'))] searchParams: [Object.assign(new RequestParam('query', 'test'))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
}); });
@@ -129,7 +129,7 @@ describe('EPersonDataService', () => {
it('search email scope and no query', () => { it('search email scope and no query', () => {
service.searchByScope('email', ''); service.searchByScope('email', '');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('email', ''))] searchParams: [Object.assign(new RequestParam('email', ''))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byEmail', options); expect(service.searchBy).toHaveBeenCalledWith('byEmail', options);
}); });

View File

@@ -15,7 +15,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { DataService } from '../data/data.service'; import { DataService } from '../data/data.service';
@@ -97,7 +97,7 @@ export class EPersonDataService extends DataService<EPerson> {
* @param linksToFollow * @param linksToFollow
*/ */
private getEpeopleByEmail(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> { private getEpeopleByEmail(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
const searchParams = [new SearchParam('email', query)]; const searchParams = [new RequestParam('email', query)];
return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, ...linksToFollow); return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, ...linksToFollow);
} }
@@ -108,7 +108,7 @@ export class EPersonDataService extends DataService<EPerson> {
* @param linksToFollow * @param linksToFollow
*/ */
private getEpeopleByMetadata(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> { private getEpeopleByMetadata(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
const searchParams = [new SearchParam('query', query)]; const searchParams = [new RequestParam('query', query)];
return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, ...linksToFollow); return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, ...linksToFollow);
} }
@@ -119,7 +119,7 @@ export class EPersonDataService extends DataService<EPerson> {
* @param options * @param options
* @param linksToFollow * @param linksToFollow
*/ */
private getEPeopleBy(searchParams: SearchParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> { private getEPeopleBy(searchParams: RequestParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
let findListOptions = new FindListOptions(); let findListOptions = new FindListOptions();
if (options) { if (options) {
findListOptions = Object.assign(new FindListOptions(), options); findListOptions = Object.assign(new FindListOptions(), options);

View File

@@ -11,7 +11,7 @@ import {
GroupRegistryEditGroupAction GroupRegistryEditGroupAction
} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; } from '../../+admin/admin-access-control/group-registry/group-registry.actions';
import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ChangeAnalyzer } from '../data/change-analyzer'; import { ChangeAnalyzer } from '../data/change-analyzer';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
@@ -103,7 +103,7 @@ describe('GroupDataService', () => {
it('search with empty query', () => { it('search with empty query', () => {
service.searchGroups(''); service.searchGroups('');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', ''))] searchParams: [Object.assign(new RequestParam('query', ''))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
}); });
@@ -111,7 +111,7 @@ describe('GroupDataService', () => {
it('search with query', () => { it('search with query', () => {
service.searchGroups('test'); service.searchGroups('test');
const options = Object.assign(new FindListOptions(), { const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new SearchParam('query', 'test'))] searchParams: [Object.assign(new RequestParam('query', 'test'))]
}); });
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
}); });

View File

@@ -14,7 +14,7 @@ import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { DataService } from '../data/data.service'; import { DataService } from '../data/data.service';
@@ -97,7 +97,7 @@ export class GroupDataService extends DataService<Group> {
* @param linksToFollow * @param linksToFollow
*/ */
public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<Group>>): Observable<RemoteData<PaginatedList<Group>>> { public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<Group>>): Observable<RemoteData<PaginatedList<Group>>> {
const searchParams = [new SearchParam('query', query)]; const searchParams = [new RequestParam('query', query)];
let findListOptions = new FindListOptions(); let findListOptions = new FindListOptions();
if (options) { if (options) {
findListOptions = Object.assign(new FindListOptions(), options); findListOptions = Object.assign(new FindListOptions(), options);
@@ -121,7 +121,7 @@ export class GroupDataService extends DataService<Group> {
isMemberOf(groupName: string): Observable<boolean> { isMemberOf(groupName: string): Observable<boolean> {
const searchHref = 'isMemberOf'; const searchHref = 'isMemberOf';
const options = new FindListOptions(); const options = new FindListOptions();
options.searchParams = [new SearchParam('groupName', groupName)]; options.searchParams = [new RequestParam('groupName', groupName)];
return this.searchBy(searchHref, options).pipe( return this.searchBy(searchHref, options).pipe(
filter((groups: RemoteData<PaginatedList<Group>>) => !groups.isResponsePending), filter((groups: RemoteData<PaginatedList<Group>>) => !groups.isResponsePending),

View File

@@ -5,27 +5,27 @@ export enum ActionType {
/** /**
* Action of reading, viewing or downloading something * Action of reading, viewing or downloading something
*/ */
READ = 0, READ = 'READ',
/** /**
* Action of modifying something * Action of modifying something
*/ */
WRITE = 1, WRITE = 'WRITE',
/** /**
* Action of deleting something * Action of deleting something
*/ */
DELETE = 2, DELETE = 'DELETE',
/** /**
* Action of adding something to a container * Action of adding something to a container
*/ */
ADD = 3, ADD = 'ADD',
/** /**
* Action of removing something from a container * Action of removing something from a container
*/ */
REMOVE = 4, REMOVE = 'REMOVE',
/** /**
* Action of performing workflow step 1 * Action of performing workflow step 1
@@ -50,15 +50,20 @@ export enum ActionType {
/** /**
* Default Read policies for Bitstreams submitted to container * Default Read policies for Bitstreams submitted to container
*/ */
DEFAULT_BITSTREAM_READ = 9, DEFAULT_BITSTREAM_READ = 'DEFAULT_BITSTREAM_READ',
/** /**
* Default Read policies for Items submitted to container * Default Read policies for Items submitted to container
*/ */
DEFAULT_ITEM_READ = 10, DEFAULT_ITEM_READ = 'DEFAULT_ITEM_READ',
/** /**
* Administrative actions * Administrative actions
*/ */
ADMIN = 11, ADMIN = 'ADMIN',
/**
* Action of withdrawn reading
*/
WITHDRAWN_READ = 'WITHDRAWN_READ'
} }

View File

@@ -0,0 +1,25 @@
/**
* Enum representing the Policy Type of a Resource Policy
*/
export enum PolicyType {
/**
* A policy in place during the submission
*/
TYPE_SUBMISSION = 'TYPE_SUBMISSION',
/**
* A policy in place during the approval workflow
*/
TYPE_WORKFLOW = 'TYPE_WORKFLOW',
/**
* A policy that has been inherited from a container (the collection)
*/
TYPE_INHERITED = 'TYPE_INHERITED',
/**
* A policy defined by the user during the submission or workflow phase
*/
TYPE_CUSTOM = 'TYPE_CUSTOM',
}

View File

@@ -0,0 +1,105 @@
import { autoserialize, deserialize, deserializeAs } from 'cerialize';
import { link, typedObject } from '../../cache/builders/build-decorators';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
import { ActionType } from './action-type.model';
import { CacheableObject } from '../../cache/object-cache.reducer';
import { HALLink } from '../../shared/hal-link.model';
import { RESOURCE_POLICY } from './resource-policy.resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { ResourceType } from '../../shared/resource-type';
import { PolicyType } from './policy-type.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../data/remote-data';
import { GROUP } from '../../eperson/models/group.resource-type';
import { Group } from '../../eperson/models/group.model';
import { EPERSON } from '../../eperson/models/eperson.resource-type';
import { EPerson } from '../../eperson/models/eperson.model';
/**
* Model class for a Resource Policy
*/
@typedObject
export class ResourcePolicy implements CacheableObject {
static type = RESOURCE_POLICY;
/**
* The identifier for this Resource Policy
*/
@autoserialize
id: string;
/**
* The name for this Resource Policy
*/
@autoserialize
name: string;
/**
* The description for this Resource Policy
*/
@autoserialize
description: string;
/**
* The classification or this Resource Policy
*/
@autoserialize
policyType: PolicyType;
/**
* The action that is allowed by this Resource Policy
*/
@autoserialize
action: ActionType;
/**
* The first day of validity of the policy (format YYYY-MM-DD)
*/
@autoserialize
startDate: string;
/**
* The last day of validity of the policy (format YYYY-MM-DD)
*/
@autoserialize
endDate: string;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The universally unique identifier for this Resource Policy
* This UUID is generated client-side and isn't used by the backend.
* It is based on the ID, so it will be the same for each refresh.
*/
@deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id')
uuid: string;
/**
* The {@link HALLink}s for this ResourcePolicy
*/
@deserialize
_links: {
eperson: HALLink,
group: HALLink,
self: HALLink,
};
/**
* The eperson linked by this resource policy
* Will be undefined unless the version {@link HALLink} has been resolved.
*/
@link(EPERSON)
eperson?: Observable<RemoteData<EPerson>>;
/**
* The group linked by this resource policy
* Will be undefined unless the version {@link HALLink} has been resolved.
*/
@link(GROUP)
group?: Observable<RemoteData<Group>>;
}

View File

@@ -1,4 +1,4 @@
import { ResourceType } from './resource-type'; import { ResourceType } from '../../shared/resource-type';
/** /**
* The resource type for ResourcePolicy * The resource type for ResourcePolicy
@@ -6,4 +6,4 @@ import { ResourceType } from './resource-type';
* Needs to be in a separate file to prevent circular * Needs to be in a separate file to prevent circular
* dependencies in webpack. * dependencies in webpack.
*/ */
export const RESOURCE_POLICY = new ResourceType('resourcePolicy'); export const RESOURCE_POLICY = new ResourceType('resourcepolicy');

View File

@@ -0,0 +1,319 @@
import { HttpClient } from '@angular/common/http';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { ResourcePolicyService } from './resource-policy.service';
import { PolicyType } from './models/policy-type.model';
import { ActionType } from './models/action-type.model';
import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model';
import { PageInfo } from '../shared/page-info.model';
import { PaginatedList } from '../data/paginated-list';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { RequestEntry } from '../data/request.reducer';
import { RestResponse } from '../cache/response.models';
describe('ResourcePolicyService', () => {
let scheduler: TestScheduler;
let service: ResourcePolicyService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let responseCacheEntry: RequestEntry;
const resourcePolicy: any = {
id: '1',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.READ,
startDate: null,
endDate: null,
type: 'resourcepolicy',
uuid: 'resource-policy-1',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
}
};
const anotherResourcePolicy: any = {
id: '2',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.WRITE,
startDate: null,
endDate: null,
type: 'resourcepolicy',
uuid: 'resource-policy-2',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
}
};
const endpointURL = `https://rest.api/rest/api/resourcepolicies`;
const requestURL = `https://rest.api/rest/api/resourcepolicies/${resourcePolicy.id}`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const resourcePolicyId = '1';
const epersonUUID = '8b39g7ya-5a4b-438b-9686-be1d5b4a1c5a';
const groupUUID = '8b39g7ya-5a4b-36987-9686-be1d5b4a1c5a';
const resourceUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a';
const pageInfo = new PageInfo();
const array = [resourcePolicy, anotherResourcePolicy];
const paginatedList = new PaginatedList(pageInfo, array);
const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy);
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
configure: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: hot('a|', {
a: resourcePolicyRD
}),
buildList: hot('a|', {
a: paginatedListRD
}),
});
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
service = new ResourcePolicyService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
http,
comparator
);
spyOn((service as any).dataService, 'create').and.callThrough();
spyOn((service as any).dataService, 'delete').and.callThrough();
spyOn((service as any).dataService, 'update').and.callThrough();
spyOn((service as any).dataService, 'findById').and.callThrough();
spyOn((service as any).dataService, 'findByHref').and.callThrough();
spyOn((service as any).dataService, 'searchBy').and.callThrough();
spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(requestURL));
});
describe('create', () => {
it('should proxy the call to dataservice.create with eperson UUID', () => {
scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID));
const params = [
new RequestParam('resource', resourceUUID),
new RequestParam('eperson', epersonUUID)
];
scheduler.flush();
expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params);
});
it('should proxy the call to dataservice.create with group UUID', () => {
scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, null, groupUUID));
const params = [
new RequestParam('resource', resourceUUID),
new RequestParam('group', groupUUID)
];
scheduler.flush();
expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params);
});
it('should return a RemoteData<ResourcePolicy> for the object with the given id', () => {
const result = service.create(resourcePolicy, resourceUUID, epersonUUID);
const expected = cold('a|', {
a: resourcePolicyRD
});
expect(result).toBeObservable(expected);
});
});
describe('delete', () => {
it('should proxy the call to dataservice.create', () => {
scheduler.schedule(() => service.delete(resourcePolicyId));
scheduler.flush();
expect((service as any).dataService.delete).toHaveBeenCalledWith(resourcePolicyId);
});
});
describe('update', () => {
it('should proxy the call to dataservice.update', () => {
scheduler.schedule(() => service.update(resourcePolicy));
scheduler.flush();
expect((service as any).dataService.update).toHaveBeenCalledWith(resourcePolicy);
});
});
describe('findById', () => {
it('should proxy the call to dataservice.findById', () => {
scheduler.schedule(() => service.findById(resourcePolicyId));
scheduler.flush();
expect((service as any).dataService.findById).toHaveBeenCalledWith(resourcePolicyId);
});
it('should return a RemoteData<ResourcePolicy> for the object with the given id', () => {
const result = service.findById(resourcePolicyId);
const expected = cold('a|', {
a: resourcePolicyRD
});
expect(result).toBeObservable(expected);
});
});
describe('findByHref', () => {
it('should proxy the call to dataservice.findByHref', () => {
scheduler.schedule(() => service.findByHref(requestURL));
scheduler.flush();
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL);
});
it('should return a RemoteData<ResourcePolicy> for the object with the given URL', () => {
const result = service.findByHref(requestURL);
const expected = cold('a|', {
a: resourcePolicyRD
});
expect(result).toBeObservable(expected);
});
});
describe('searchByEPerson', () => {
it('should proxy the call to dataservice.searchBy', () => {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', epersonUUID)];
scheduler.schedule(() => service.searchByEPerson(epersonUUID));
scheduler.flush();
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options);
});
it('should proxy the call to dataservice.searchBy with additional search param', () => {
const options = new FindListOptions();
options.searchParams = [
new RequestParam('uuid', epersonUUID),
new RequestParam('resource', resourceUUID),
];
scheduler.schedule(() => service.searchByEPerson(epersonUUID, resourceUUID));
scheduler.flush();
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options);
});
it('should return a RemoteData<PaginatedList<ResourcePolicy>) for the search', () => {
const result = service.searchByEPerson(epersonUUID, resourceUUID);
const expected = cold('a|', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
describe('searchByGroup', () => {
it('should proxy the call to dataservice.searchBy', () => {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', groupUUID)];
scheduler.schedule(() => service.searchByGroup(groupUUID));
scheduler.flush();
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options);
});
it('should proxy the call to dataservice.searchBy with additional search param', () => {
const options = new FindListOptions();
options.searchParams = [
new RequestParam('uuid', groupUUID),
new RequestParam('resource', resourceUUID),
];
scheduler.schedule(() => service.searchByGroup(groupUUID, resourceUUID));
scheduler.flush();
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options);
});
it('should return a RemoteData<PaginatedList<ResourcePolicy>) for the search', () => {
const result = service.searchByGroup(groupUUID);
const expected = cold('a|', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
describe('searchByResource', () => {
it('should proxy the call to dataservice.searchBy', () => {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', resourceUUID)];
scheduler.schedule(() => service.searchByResource(resourceUUID));
scheduler.flush();
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options);
});
it('should proxy the call to dataservice.searchBy with additional search param', () => {
const action = ActionType.READ;
const options = new FindListOptions();
options.searchParams = [
new RequestParam('uuid', resourceUUID),
new RequestParam('action', action),
];
scheduler.schedule(() => service.searchByResource(resourceUUID, action));
scheduler.flush();
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options);
});
it('should return a RemoteData<PaginatedList<ResourcePolicy>) for the search', () => {
const result = service.searchByResource(resourceUUID);
const expected = cold('a|', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
});

View File

@@ -0,0 +1,193 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { FindListOptions } from '../data/request.models';
import { Collection } from '../shared/collection.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResourcePolicy } from './models/resource-policy.model';
import { RemoteData } from '../data/remote-data';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RESOURCE_POLICY } from './models/resource-policy.resource-type';
import { ChangeAnalyzer } from '../data/change-analyzer';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list';
import { ActionType } from './models/action-type.model';
import { RequestParam } from '../cache/models/request-param.model';
import { isNotEmpty } from '../../shared/empty.util';
/* tslint:disable:max-classes-per-file */
/**
* A private DataService implementation to delegate specific methods to.
*/
class DataServiceImpl extends DataService<ResourcePolicy> {
protected linkPath = 'resourcepolicies';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: ChangeAnalyzer<ResourcePolicy>) {
super();
}
}
/**
* A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint
*/
@Injectable()
@dataService(RESOURCE_POLICY)
export class ResourcePolicyService {
private dataService: DataServiceImpl;
protected searchByEPersonMethod = 'eperson';
protected searchByGroupMethod = 'group';
protected searchByResourceMethod = 'resource';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ResourcePolicy>) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
}
/**
* Create a new ResourcePolicy on the server, and store the response
* in the object cache
*
* @param {ResourcePolicy} resourcePolicy
* The resource policy to create
* @param {string} resourceUUID
* The uuid of the resource target of the policy
* @param {string} epersonUUID
* The uuid of the eperson that will be grant of the permission. Exactly one of eperson or group is required
* @param {string} groupUUID
* The uuid of the group that will be grant of the permission. Exactly one of eperson or group is required
*/
create(resourcePolicy: ResourcePolicy, resourceUUID: string, epersonUUID?: string, groupUUID?: string): Observable<RemoteData<ResourcePolicy>> {
const params = [];
params.push(new RequestParam('resource', resourceUUID));
if (isNotEmpty(epersonUUID)) {
params.push(new RequestParam('eperson', epersonUUID));
} else if (isNotEmpty(groupUUID)) {
params.push(new RequestParam('group', groupUUID));
}
return this.dataService.create(resourcePolicy, ...params);
}
/**
* Delete an existing ResourcePolicy on the server
*
* @param resourcePolicyID The resource policy's id to be removed
* @return an observable that emits true when the deletion was successful, false when it failed
*/
delete(resourcePolicyID: string): Observable<boolean> {
return this.dataService.delete(resourcePolicyID);
}
/**
* Add a new patch to the object cache
* The patch is derived from the differences between the given object and its version in the object cache
* @param {ResourcePolicy} object The given object
*/
update(object: ResourcePolicy): Observable<RemoteData<ResourcePolicy>> {
return this.dataService.update(object);
}
/**
* Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the {@link ResourcePolicy}
* @param href The url of {@link ResourcePolicy} we want to retrieve
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<ResourcePolicy>> {
return this.dataService.findByHref(href, ...linksToFollow);
}
/**
* Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on its ID, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object
* @param id ID of {@link ResourcePolicy} we want to retrieve
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findById(id: string, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<ResourcePolicy>> {
return this.dataService.findById(id, ...linksToFollow);
}
/**
* Return the defaultAccessConditions {@link ResourcePolicy} list for a given {@link Collection}
*
* @param collection the {@link Collection} to retrieve the defaultAccessConditions for
* @param findListOptions the {@link FindListOptions} for the request
*/
getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions);
}
/**
* Return the {@link ResourcePolicy} list for a {@link EPerson}
*
* @param UUID UUID of a given {@link EPerson}
* @param resourceUUID Limit the returned policies to the specified DSO
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
searchByEPerson(UUID: string, resourceUUID?: string, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', UUID)];
if (isNotEmpty(resourceUUID)) {
options.searchParams.push(new RequestParam('resource', resourceUUID))
}
return this.dataService.searchBy(this.searchByEPersonMethod, options, ...linksToFollow)
}
/**
* Return the {@link ResourcePolicy} list for a {@link Group}
*
* @param UUID UUID of a given {@link Group}
* @param resourceUUID Limit the returned policies to the specified DSO
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
searchByGroup(UUID: string, resourceUUID?: string, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', UUID)];
if (isNotEmpty(resourceUUID)) {
options.searchParams.push(new RequestParam('resource', resourceUUID))
}
return this.dataService.searchBy(this.searchByGroupMethod, options, ...linksToFollow)
}
/**
* Return the {@link ResourcePolicy} list for a given DSO
*
* @param UUID UUID of a given DSO
* @param action Limit the returned policies to the specified {@link ActionType}
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
searchByResource(UUID: string, action?: ActionType, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
const options = new FindListOptions();
options.searchParams = [new RequestParam('uuid', UUID)];
if (isNotEmpty(action)) {
options.searchParams.push(new RequestParam('action', action))
}
return this.dataService.searchBy(this.searchByResourceMethod, options, ...linksToFollow)
}
}

View File

@@ -1,8 +1,15 @@
import { deserialize, inheritSerialization } from 'cerialize'; import { deserialize, inheritSerialization } from 'cerialize';
import { typedObject } from '../cache/builders/build-decorators';
import { Observable } from 'rxjs';
import { link, typedObject } from '../cache/builders/build-decorators';
import { BUNDLE } from './bundle.resource-type'; import { BUNDLE } from './bundle.resource-type';
import { DSpaceObject } from './dspace-object.model'; import { DSpaceObject } from './dspace-object.model';
import { HALLink } from './hal-link.model'; import { HALLink } from './hal-link.model';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list';
import { BITSTREAM } from './bitstream.resource-type';
import { Bitstream } from './bitstream.model';
@typedObject @typedObject
@inheritSerialization(DSpaceObject) @inheritSerialization(DSpaceObject)
@@ -17,5 +24,19 @@ export class Bundle extends DSpaceObject {
self: HALLink; self: HALLink;
primaryBitstream: HALLink; primaryBitstream: HALLink;
bitstreams: HALLink; bitstreams: HALLink;
} };
/**
* The primary Bitstream of this Bundle
* Will be undefined unless the primaryBitstream {@link HALLink} has been resolved.
*/
@link(BITSTREAM)
primaryBitstream?: Observable<RemoteData<Bitstream>>;
/**
* The list of Bitstreams that are direct children of this Bundle
* Will be undefined unless the bitstreams {@link HALLink} has been resolved.
*/
@link(BITSTREAM, true)
bitstreams?: Observable<RemoteData<PaginatedList<Bitstream>>>;
} }

View File

@@ -10,8 +10,8 @@ import { DSpaceObject } from './dspace-object.model';
import { HALLink } from './hal-link.model'; import { HALLink } from './hal-link.model';
import { License } from './license.model'; import { License } from './license.model';
import { LICENSE } from './license.resource-type'; import { LICENSE } from './license.resource-type';
import { ResourcePolicy } from './resource-policy.model'; import { ResourcePolicy } from '../resource-policy/models/resource-policy.model';
import { RESOURCE_POLICY } from './resource-policy.resource-type'; import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resource-type';
import { COMMUNITY } from './community.resource-type'; import { COMMUNITY } from './community.resource-type';
import { Community } from './community.model'; import { Community } from './community.model';
import { ChildHALResource } from './child-hal-resource.model'; import { ChildHALResource } from './child-hal-resource.model';

View File

@@ -67,6 +67,10 @@ export const getSucceededRemoteData = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> => <T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded)); source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
export const getSucceededRemoteWithNotEmptyData = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded && isNotEmpty(rd.payload)));
/** /**
* Get the first successful remotely retrieved object * Get the first successful remotely retrieved object
* *
@@ -84,6 +88,23 @@ export const getFirstSucceededRemoteDataPayload = () =>
getRemoteDataPayload() getRemoteDataPayload()
); );
/**
* Get the first successful remotely retrieved object with not empty payload
*
* You usually don't want to use this, it is a code smell.
* Work with the RemoteData object instead, that way you can
* handle loading and errors correctly.
*
* These operators were created as a first step in refactoring
* out all the instances where this is used incorrectly.
*/
export const getFirstSucceededRemoteDataWithNotEmptyPayload = () =>
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
source.pipe(
getSucceededRemoteWithNotEmptyData(),
getRemoteDataPayload()
);
/** /**
* Get the all successful remotely retrieved objects * Get the all successful remotely retrieved objects
* *

View File

@@ -1,58 +0,0 @@
import { autoserialize, deserialize, deserializeAs } from 'cerialize';
import { typedObject } from '../cache/builders/build-decorators';
import { IDToUUIDSerializer } from '../cache/id-to-uuid-serializer';
import { ActionType } from '../cache/models/action-type.model';
import { CacheableObject } from '../cache/object-cache.reducer';
import { excludeFromEquals } from '../utilities/equals.decorators';
import { HALLink } from './hal-link.model';
import { RESOURCE_POLICY } from './resource-policy.resource-type';
import { ResourceType } from './resource-type';
/**
* Model class for a Resource Policy
*/
@typedObject
export class ResourcePolicy implements CacheableObject {
static type = RESOURCE_POLICY;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The action that is allowed by this Resource Policy
*/
@autoserialize
action: ActionType;
/**
* The name for this Resource Policy
*/
@autoserialize
name: string;
/**
* The uuid of the Group this Resource Policy applies to
*/
@autoserialize
groupUUID: string;
/**
* The universally unique identifier for this Resource Policy
* This UUID is generated client-side and isn't used by the backend.
* It is based on the ID, so it will be the same for each refresh.
*/
@deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id')
uuid: string;
/**
* The {@link HALLink}s for this ResourcePolicy
*/
@deserialize
_links: {
self: HALLink,
}
}

View File

@@ -13,6 +13,7 @@ import { getSucceededRemoteData } from '../../../core/shared/operators';
import { ResourceType } from '../../../core/shared/resource-type'; import { ResourceType } from '../../../core/shared/resource-type';
import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { RequestParam } from '../../../core/cache/models/request-param.model';
/** /**
* Component representing the create page for communities and collections * Component representing the create page for communities and collections
@@ -76,7 +77,7 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
const uploader = event.uploader; const uploader = event.uploader;
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
this.dsoDataService.create(dso, uuid) this.dsoDataService.create(dso, new RequestParam('parent', uuid))
.pipe(getSucceededRemoteData()) .pipe(getSucceededRemoteData())
.subscribe((dsoRD: RemoteData<TDomain>) => { .subscribe((dsoRD: RemoteData<TDomain>) => {
if (isNotUndefined(dsoRD)) { if (isNotUndefined(dsoRD)) {

View File

@@ -3,6 +3,8 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { isObject } from 'lodash'; import { isObject } from 'lodash';
import * as moment from 'moment'; import * as moment from 'moment';
import { isNull } from './empty.util';
/** /**
* Returns true if the passed value is a NgbDateStruct. * Returns true if the passed value is a NgbDateStruct.
* *
@@ -56,3 +58,57 @@ export function dateToISOFormat(date: Date | NgbDateStruct): string {
export function ngbDateStructToDate(date: NgbDateStruct): Date { export function ngbDateStructToDate(date: NgbDateStruct): Date {
return new Date(date.year, (date.month - 1), date.day); return new Date(date.year, (date.month - 1), date.day);
} }
/**
* Returns a NgbDateStruct object started from a string representing a date
*
* @param date
* The Date to convert
* @return NgbDateStruct
* the NgbDateStruct object
*/
export function stringToNgbDateStruct(date: string): NgbDateStruct {
return dateToNgbDateStruct(new Date(date));
}
/**
* Returns a NgbDateStruct object started from a Date object
*
* @param date
* The Date to convert
* @return NgbDateStruct
* the NgbDateStruct object
*/
export function dateToNgbDateStruct(date?: Date): NgbDateStruct {
if (isNull(date)) {
date = new Date()
}
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate()
};
}
/**
* Returns a date in simplified format (YYYY-MM-DD).
*
* @param date
* The date to format
* @return string
* the formatted date
*/
export function dateToString(date: Date | NgbDateStruct): string {
const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date);
let year = dateObj.getFullYear().toString();
let month = (dateObj.getMonth() + 1).toString();
let day = dateObj.getDate().toString();
year = (year.length === 1) ? '0' + year : year;
month = (month.length === 1) ? '0' + month : month;
day = (day.length === 1) ? '0' + day : day;
const dateStr = `${year}-${month}-${day}`;
return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD');
}

View File

@@ -0,0 +1,10 @@
import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service';
export function getMockResourcePolicyService(): ResourcePolicyService {
return jasmine.createSpyObj('resourcePolicyService', {
searchByResource: jasmine.createSpy('searchByResource'),
create: jasmine.createSpy('create'),
delete: jasmine.createSpy('delete'),
update: jasmine.createSpy('update')
});
}

View File

@@ -0,0 +1,16 @@
import { Directive, Host } from '@angular/core';
import { NgForOf } from '@angular/common';
import { DSpaceObject } from '../core/shared/dspace-object.model';
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[ngForTrackById]',
})
export class NgForTrackByIdDirective<T extends DSpaceObject> {
constructor(@Host() private ngFor: NgForOf<T>) {
this.ngFor.ngForTrackBy = (index: number, dso: T) => (dso) ? dso.id : undefined;
}
}

View File

@@ -0,0 +1,7 @@
<div class="container">
<h4 class="mb-3">{{'resource-policies.create.page.heading' | translate}} {{targetResourceName}}</h4>
<ds-resource-policy-form [isProcessing]="isProcessing()"
(reset)="redirectToAuthorizationsPage()"
(submit)="createResourcePolicy($event)"></ds-resource-policy-form>
</div>

View File

@@ -0,0 +1,265 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { TranslateModule } from '@ngx-translate/core';
import {
createFailedRemoteDataObject,
createSuccessfulRemoteDataObject
} from '../../remote-data.utils';
import { createTestComponent } from '../../testing/utils.test';
import { ResourcePolicyCreateComponent } from './resource-policy-create.component';
import { LinkService } from '../../../core/cache/builders/link.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { getMockResourcePolicyService } from '../../mocks/mock-resource-policy-service';
import { getMockLinkService } from '../../mocks/link-service.mock';
import { RouterStub } from '../../testing/router.stub';
import { Item } from '../../../core/shared/item.model';
import { createMockRDPaginatedObs } from '../../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec';
import { ResourcePolicyEvent } from '../form/resource-policy-form.component';
import { GroupMock } from '../../testing/group-mock';
import { submittedResourcePolicy } from '../form/resource-policy-form.component.spec';
import { PolicyType } from '../../../core/resource-policy/models/policy-type.model';
import { ActionType } from '../../../core/resource-policy/models/action-type.model';
import { EPersonMock } from '../../testing/eperson.mock';
describe('ResourcePolicyCreateComponent test suite', () => {
let comp: ResourcePolicyCreateComponent;
let compAsAny: any;
let fixture: ComponentFixture<ResourcePolicyCreateComponent>;
let de;
let scheduler: TestScheduler;
let eventPayload: ResourcePolicyEvent;
const resourcePolicy: any = {
id: '1',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.READ,
startDate: null,
endDate: null,
type: 'resourcepolicy',
uuid: 'resource-policy-1',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
},
eperson: observableOf(createSuccessfulRemoteDataObject({})),
group: observableOf(createSuccessfulRemoteDataObject(GroupMock))
};
const item = Object.assign(new Item(), {
uuid: 'itemUUID',
id: 'itemUUID',
metadata: {
'dc.title': [{
value: 'test item'
}]
},
_links: {
self: { href: 'item-selflink' }
},
bundles: createMockRDPaginatedObs([])
});
const resourcePolicyService: any = getMockResourcePolicyService();
const linkService: any = getMockLinkService();
const routeStub = {
data: observableOf({
resourcePolicyTarget: createSuccessfulRemoteDataObject(item)
})
};
const routerStub = Object.assign(new RouterStub(), {
url: `url/edit`
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot()
],
declarations: [
ResourcePolicyCreateComponent,
TestComponent
],
providers: [
{ provide: LinkService, useValue: linkService },
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: ResourcePolicyService, useValue: resourcePolicyService },
{ provide: Router, useValue: routerStub },
ResourcePolicyCreateComponent,
ChangeDetectorRef,
Injector
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-resource-policy-create></ds-resource-policy-create>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create ResourcePolicyCreateComponent', inject([ResourcePolicyCreateComponent], (app: ResourcePolicyCreateComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(ResourcePolicyCreateComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should init component properly', () => {
fixture.detectChanges();
expect(compAsAny.targetResourceUUID).toBe('itemUUID');
expect(compAsAny.targetResourceName).toBe('test item');
});
it('should redirect to authorizations page', () => {
comp.redirectToAuthorizationsPage();
expect(compAsAny.router.navigate).toHaveBeenCalled();
});
it('should return true when is Processing', () => {
compAsAny.processing$.next(true);
expect(comp.isProcessing()).toBeObservable(cold('a', {
a: true
}));
});
it('should return false when is not Processing', () => {
compAsAny.processing$.next(false);
expect(comp.isProcessing()).toBeObservable(cold('a', {
a: false
}));
});
describe('when target type is group', () => {
beforeEach(() => {
spyOn(comp, 'redirectToAuthorizationsPage').and.callThrough();
compAsAny.targetResourceUUID = 'itemUUID';
eventPayload = Object.create({});
eventPayload.object = submittedResourcePolicy;
eventPayload.target = {
type: 'group',
uuid: GroupMock.id
};
});
it('should notify success when creation is successful', () => {
compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy)));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.createResourcePolicy(eventPayload));
scheduler.flush();
expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', null, eventPayload.target.uuid);
expect(comp.redirectToAuthorizationsPage).toHaveBeenCalled();
});
it('should notify error when creation is not successful', () => {
compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createFailedRemoteDataObject({})));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.createResourcePolicy(eventPayload));
scheduler.flush();
expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', null, eventPayload.target.uuid);
expect(comp.redirectToAuthorizationsPage).not.toHaveBeenCalled();
});
});
describe('when target type of created policy is eperson', () => {
beforeEach(() => {
spyOn(comp, 'redirectToAuthorizationsPage').and.callThrough();
compAsAny.targetResourceUUID = 'itemUUID';
eventPayload = Object.create({});
eventPayload.object = submittedResourcePolicy;
eventPayload.target = {
type: 'eperson',
uuid: EPersonMock.id
};
});
it('should notify success when creation is successful', () => {
compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy)));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.createResourcePolicy(eventPayload));
scheduler.flush();
expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', eventPayload.target.uuid);
expect(comp.redirectToAuthorizationsPage).toHaveBeenCalled();
});
it('should notify error when creation is not successful', () => {
compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createFailedRemoteDataObject({})));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.createResourcePolicy(eventPayload));
scheduler.flush();
expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', eventPayload.target.uuid);
expect(comp.redirectToAuthorizationsPage).not.toHaveBeenCalled();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
}

View File

@@ -0,0 +1,112 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, map, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { RemoteData } from '../../../core/data/remote-data';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { ResourcePolicyEvent } from '../form/resource-policy-form.component';
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-resource-policy-create',
templateUrl: './resource-policy-create.component.html'
})
export class ResourcePolicyCreateComponent implements OnInit {
/**
* The name of the resource target of the policy
*/
public targetResourceName: string;
/**
* A boolean representing if a submission creation operation is pending
* @type {BehaviorSubject<boolean>}
*/
private processing$ = new BehaviorSubject<boolean>(false);
/**
* The uuid of the resource target of the policy
*/
private targetResourceUUID: string;
/**
* Initialize instance variables
*
* @param {DSONameService} dsoNameService
* @param {NotificationsService} notificationsService
* @param {ResourcePolicyService} resourcePolicyService
* @param {ActivatedRoute} route
* @param {Router} router
* @param {TranslateService} translate
*/
constructor(
private dsoNameService: DSONameService,
private notificationsService: NotificationsService,
private resourcePolicyService: ResourcePolicyService,
private route: ActivatedRoute,
private router: Router,
private translate: TranslateService) {
}
/**
* Initialize the component
*/
ngOnInit(): void {
this.route.data.pipe(
map((data) => data),
take(1)
).subscribe((data: any) => {
this.targetResourceUUID = (data.resourcePolicyTarget as RemoteData<DSpaceObject>).payload.id;
this.targetResourceName = this.dsoNameService.getName((data.resourcePolicyTarget as RemoteData<DSpaceObject>).payload);
});
}
/**
* Return a boolean representing if an operation is pending
*
* @return {Observable<boolean>}
*/
isProcessing(): Observable<boolean> {
return this.processing$.asObservable();
}
/**
* Redirect to the authorizations page
*/
redirectToAuthorizationsPage(): void {
this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route });
}
/**
* Create a new resource policy
*
* @param event The {{ResourcePolicyEvent}} emitted
*/
createResourcePolicy(event: ResourcePolicyEvent): void {
this.processing$.next(true);
let response$;
if (event.target.type === 'eperson') {
response$ = this.resourcePolicyService.create(event.object, this.targetResourceUUID, event.target.uuid);
} else {
response$ = this.resourcePolicyService.create(event.object, this.targetResourceUUID, null, event.target.uuid);
}
response$.pipe(
first((response: RemoteData<ResourcePolicy>) => !response.isResponsePending)
).subscribe((responseRD: RemoteData<ResourcePolicy>) => {
this.processing$.next(false);
if (responseRD.hasSucceeded) {
this.notificationsService.success(null, this.translate.get('resource-policies.create.page.success.content'));
this.redirectToAuthorizationsPage();
} else {
this.notificationsService.error(null, this.translate.get('resource-policies.create.page.failure.content'));
}
})
}
}

View File

@@ -0,0 +1,8 @@
<div class="container">
<h4 class="mb-3">{{'resource-policies.edit.page.heading' | translate}} {{resourcePolicy.id}}</h4>
<ds-resource-policy-form [resourcePolicy]="resourcePolicy"
[isProcessing]="isProcessing()"
(reset)="redirectToAuthorizationsPage()"
(submit)="updateResourcePolicy($event)"></ds-resource-policy-form>
</div>

View File

@@ -0,0 +1,220 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { TranslateModule } from '@ngx-translate/core';
import {
createFailedRemoteDataObject,
createSuccessfulRemoteDataObject
} from '../../remote-data.utils';
import { createTestComponent } from '../../testing/utils.test';
import { LinkService } from '../../../core/cache/builders/link.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { getMockResourcePolicyService } from '../../mocks/mock-resource-policy-service';
import { getMockLinkService } from '../../mocks/link-service.mock';
import { RouterStub } from '../../testing/router.stub';
import { ResourcePolicyEvent } from '../form/resource-policy-form.component';
import { GroupMock } from '../../testing/group-mock';
import { submittedResourcePolicy } from '../form/resource-policy-form.component.spec';
import { PolicyType } from '../../../core/resource-policy/models/policy-type.model';
import { ActionType } from '../../../core/resource-policy/models/action-type.model';
import { ResourcePolicyEditComponent } from './resource-policy-edit.component';
import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type';
describe('ResourcePolicyEditComponent test suite', () => {
let comp: ResourcePolicyEditComponent;
let compAsAny: any;
let fixture: ComponentFixture<ResourcePolicyEditComponent>;
let de;
let scheduler: TestScheduler;
let eventPayload: ResourcePolicyEvent;
let updatedObject;
const resourcePolicy: any = {
id: '1',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.READ,
startDate: null,
endDate: null,
type: 'resourcepolicy',
uuid: 'resource-policy-1',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
},
eperson: observableOf(createSuccessfulRemoteDataObject({})),
group: observableOf(createSuccessfulRemoteDataObject(GroupMock))
};
const resourcePolicyService: any = getMockResourcePolicyService();
const linkService: any = getMockLinkService();
const routeStub = {
data: observableOf({
resourcePolicy: createSuccessfulRemoteDataObject(resourcePolicy)
})
};
const routerStub = Object.assign(new RouterStub(), {
url: `url/edit`
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot()
],
declarations: [
ResourcePolicyEditComponent,
TestComponent
],
providers: [
{ provide: LinkService, useValue: linkService },
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: ResourcePolicyService, useValue: resourcePolicyService },
{ provide: Router, useValue: routerStub },
ResourcePolicyEditComponent,
ChangeDetectorRef,
Injector
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-resource-policy-edit></ds-resource-policy-edit>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create ResourcePolicyEditComponent', inject([ResourcePolicyEditComponent], (app: ResourcePolicyEditComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(ResourcePolicyEditComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should init component properly', () => {
fixture.detectChanges();
expect(compAsAny.resourcePolicy).toEqual(resourcePolicy);
});
it('should redirect to authorizations page', () => {
comp.redirectToAuthorizationsPage();
expect(compAsAny.router.navigate).toHaveBeenCalled();
});
it('should return true when is Processing', () => {
compAsAny.processing$.next(true);
expect(comp.isProcessing()).toBeObservable(cold('a', {
a: true
}));
});
it('should return false when is not Processing', () => {
compAsAny.processing$.next(false);
expect(comp.isProcessing()).toBeObservable(cold('a', {
a: false
}));
});
describe('', () => {
beforeEach(() => {
spyOn(comp, 'redirectToAuthorizationsPage').and.callThrough();
compAsAny.resourcePolicyService.update.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy)));
compAsAny.targetResourceUUID = 'itemUUID';
eventPayload = Object.create({});
eventPayload.object = submittedResourcePolicy;
eventPayload.target = {
type: 'group',
uuid: GroupMock.id
};
compAsAny.resourcePolicy = resourcePolicy;
updatedObject = Object.assign({}, submittedResourcePolicy, {
id: resourcePolicy.id,
type: RESOURCE_POLICY.value,
_links: resourcePolicy._links
});
});
it('should notify success when update is successful', () => {
compAsAny.resourcePolicyService.update.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy)));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.updateResourcePolicy(eventPayload));
scheduler.flush();
expect(compAsAny.resourcePolicyService.update).toHaveBeenCalledWith(updatedObject);
expect(comp.redirectToAuthorizationsPage).toHaveBeenCalled();
});
it('should notify error when update is not successful', () => {
compAsAny.resourcePolicyService.update.and.returnValue(observableOf(createFailedRemoteDataObject({})));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.updateResourcePolicy(eventPayload));
scheduler.flush();
expect(compAsAny.resourcePolicyService.update).toHaveBeenCalledWith(updatedObject);
expect(comp.redirectToAuthorizationsPage).not.toHaveBeenCalled();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
}

View File

@@ -0,0 +1,102 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, map, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { RemoteData } from '../../../core/data/remote-data';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { ResourcePolicyEvent } from '../form/resource-policy-form.component';
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type';
@Component({
selector: 'ds-resource-policy-edit',
templateUrl: './resource-policy-edit.component.html'
})
export class ResourcePolicyEditComponent implements OnInit {
/**
* The resource policy object to edit
*/
public resourcePolicy: ResourcePolicy;
/**
* A boolean representing if a submission editing operation is pending
* @type {BehaviorSubject<boolean>}
*/
private processing$ = new BehaviorSubject<boolean>(false);
/**
* Initialize instance variables
*
* @param {NotificationsService} notificationsService
* @param {ResourcePolicyService} resourcePolicyService
* @param {ActivatedRoute} route
* @param {Router} router
* @param {TranslateService} translate
*/
constructor(
private notificationsService: NotificationsService,
private resourcePolicyService: ResourcePolicyService,
private route: ActivatedRoute,
private router: Router,
private translate: TranslateService) {
}
/**
* Initialize the component
*/
ngOnInit(): void {
this.route.data.pipe(
map((data) => data),
take(1)
).subscribe((data: any) => {
this.resourcePolicy = (data.resourcePolicy as RemoteData<ResourcePolicy>).payload;
});
}
/**
* Return a boolean representing if an operation is pending
*
* @return {Observable<boolean>}
*/
isProcessing(): Observable<boolean> {
return this.processing$.asObservable();
}
/**
* Redirect to the authorizations page
*/
redirectToAuthorizationsPage() {
this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route });
}
/**
* Update a resource policy
*
* @param event The {{ResourcePolicyEvent}} emitted
*/
updateResourcePolicy(event: ResourcePolicyEvent) {
this.processing$.next(true);
const updatedObject = Object.assign({}, event.object, {
id: this.resourcePolicy.id,
type: RESOURCE_POLICY.value,
_links: this.resourcePolicy._links
});
this.resourcePolicyService.update(updatedObject).pipe(
first((response: RemoteData<ResourcePolicy>) => !response.isResponsePending)
).subscribe((responseRD: RemoteData<ResourcePolicy>) => {
this.processing$.next(false);
if (responseRD.hasSucceeded) {
this.notificationsService.success(null, this.translate.get('resource-policies.edit.page.success.content'));
this.redirectToAuthorizationsPage();
} else {
this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.failure.content'));
}
})
}
}

View File

@@ -0,0 +1,37 @@
<div class="mt-3" @fadeInOut>
<ds-eperson-search-box *ngIf="isListOfEPerson" (search)="onSearch($event)"></ds-eperson-search-box>
<ds-group-search-box *ngIf="!isListOfEPerson" (search)="onSearch($event)"></ds-group-search-box>
<ds-pagination *ngIf="(getList() | async)?.payload?.totalElements > 0"
[paginationOptions]="paginationOptions"
[collectionSize]="(getList() | async)?.payload?.totalElements"
[disableRouteParameterUpdate]="true"
[hideGear]="true"
(pageChange)="onPageChange($event)">
<div class="table-responsive">
<table id="groups" class="table table-sm table-striped table-hover table-bordered">
<thead>
<tr class="text-center">
<th>{{'resource-policies.form.eperson-group-list.table.headers.id' | translate}}</th>
<th>{{'resource-policies.form.eperson-group-list.table.headers.name' | translate}}</th>
<th>{{'resource-policies.form.eperson-group-list.table.headers.action' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let entry of (getList() | async)?.payload?.page"
[class.table-primary]="isSelected(entry) | async">
<td>{{entry.id}}</td>
<td>{{dsoNameService.getName(entry)}}</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary" (click)="emitSelect(entry)">
{{'resource-policies.form.eperson-group-list.select.btn' | translate}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
</div>

View File

@@ -0,0 +1,287 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { uniqueId } from 'lodash';
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { createTestComponent } from '../../../testing/utils.test';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { RequestService } from '../../../../core/data/request.service';
import { getMockRequestService } from '../../../mocks/request.service.mock';
import { EpersonGroupListComponent, SearchEvent } from './eperson-group-list.component';
import { EPersonMock } from '../../../testing/eperson.mock';
import { GroupMock } from '../../../testing/group-mock';
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('EpersonGroupListComponent test suite', () => {
let comp: EpersonGroupListComponent;
let compAsAny: any;
let fixture: ComponentFixture<EpersonGroupListComponent>;
let de;
let groupService: any;
let epersonService: any;
const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions()
paginationOptions.id = uniqueId('eperson-group-list-pagination-test');
paginationOptions.pageSize = 5;
const mockEpersonService = jasmine.createSpyObj('epersonService',
{
findByHref: jasmine.createSpy('findByHref'),
findAll: jasmine.createSpy('findAll'),
searchByScope: jasmine.createSpy('searchByScope'),
},
{
linkPath: 'epersons'
}
);
const mockGroupService = jasmine.createSpyObj('groupService',
{
findByHref: jasmine.createSpy('findByHref'),
findAll: jasmine.createSpy('findAll'),
searchGroups: jasmine.createSpy('searchGroups'),
},
{
linkPath: 'groups'
}
);
const epersonPaginatedList = new PaginatedList(new PageInfo(), [EPersonMock, EPersonMock]);
const epersonPaginatedListRD = createSuccessfulRemoteDataObject(epersonPaginatedList);
const groupPaginatedList = new PaginatedList(new PageInfo(), [GroupMock, GroupMock]);
const groupPaginatedListRD = createSuccessfulRemoteDataObject(groupPaginatedList);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot()
],
declarations: [
EpersonGroupListComponent,
TestComponent
],
providers: [
{ provide: EPersonDataService, useValue: mockEpersonService },
{ provide: GroupDataService, useValue: mockGroupService },
{ provide: RequestService, useValue: getMockRequestService() },
EpersonGroupListComponent,
ChangeDetectorRef,
Injector
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-eperson-group-list [isListOfEPerson]="isListOfEPerson" [initSelected]="initSelected"></ds-eperson-group-list>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create EpersonGroupListComponent', inject([EpersonGroupListComponent], (app: EpersonGroupListComponent) => {
expect(app).toBeDefined();
}));
});
describe('when is list of eperson', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(EpersonGroupListComponent);
epersonService = TestBed.get(EPersonDataService);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
comp.isListOfEPerson = true;
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should inject EPersonDataService', () => {
spyOn(comp, 'updateList');
fixture.detectChanges();
expect(compAsAny.dataService).toBeDefined();
expect(comp.updateList).toHaveBeenCalled();
});
it('should init entrySelectedId', () => {
spyOn(comp, 'updateList');
comp.initSelected = EPersonMock.id;
fixture.detectChanges();
expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id)
});
it('should init the list of eperson', () => {
epersonService.searchByScope.and.returnValue(observableOf(epersonPaginatedListRD));
fixture.detectChanges();
expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD);
expect(comp.getList()).toBeObservable(cold('a', {
a: epersonPaginatedListRD
}));
});
it('should emit select event', () => {
spyOn(comp.select, 'emit');
comp.emitSelect(EPersonMock);
expect(comp.select.emit).toHaveBeenCalled();
expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id);
});
it('should return true when entry is selected', () => {
compAsAny.entrySelectedId.next(EPersonMock.id);
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: true
}));
});
it('should return false when entry is not selected', () => {
compAsAny.entrySelectedId.next('');
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: false
}));
});
it('should update list on page change', () => {
spyOn(comp, 'updateList');
comp.onPageChange(2);
expect(compAsAny.updateList).toHaveBeenCalled();
});
});
describe('when is list of group', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(EpersonGroupListComponent);
groupService = TestBed.get(GroupDataService);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
comp.isListOfEPerson = false;
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should inject GroupDataService', () => {
spyOn(comp, 'updateList');
fixture.detectChanges();
expect(compAsAny.dataService).toBeDefined();
expect(comp.updateList).toHaveBeenCalled();
});
it('should init entrySelectedId', () => {
spyOn(comp, 'updateList');
comp.initSelected = GroupMock.id;
fixture.detectChanges();
expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id)
});
it('should init the list of group', () => {
groupService.searchGroups.and.returnValue(observableOf(groupPaginatedListRD));
fixture.detectChanges();
expect(compAsAny.list$.value).toEqual(groupPaginatedListRD);
expect(comp.getList()).toBeObservable(cold('a', {
a: groupPaginatedListRD
}));
});
it('should emit select event', () => {
spyOn(comp.select, 'emit');
comp.emitSelect(GroupMock);
expect(comp.select.emit).toHaveBeenCalled();
expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id);
});
it('should return true when entry is selected', () => {
compAsAny.entrySelectedId.next(EPersonMock.id);
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: true
}));
});
it('should return false when entry is not selected', () => {
compAsAny.entrySelectedId.next('');
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: false
}));
});
it('should update list on page change', () => {
spyOn(comp, 'updateList');
comp.onPageChange(2);
expect(compAsAny.updateList).toHaveBeenCalled();
});
it('should update list on search triggered', () => {
const options: PaginationComponentOptions = comp.paginationOptions
const event: SearchEvent = {
scope: 'metadata',
query: 'test'
}
spyOn(comp, 'updateList');
comp.onSearch(event);
expect(compAsAny.updateList).toHaveBeenCalledWith(options, 'metadata', 'test');
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
isListOfEPerson = true;
initSelected = '';
}

View File

@@ -0,0 +1,201 @@
import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { uniqueId } from 'lodash'
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
import { DataService } from '../../../../core/data/data.service';
import { hasValue, isNotEmpty } from '../../../empty.util';
import { FindListOptions } from '../../../../core/data/request.models';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { getDataServiceFor } from '../../../../core/cache/builders/build-decorators';
import { EPERSON } from '../../../../core/eperson/models/eperson.resource-type';
import { GROUP } from '../../../../core/eperson/models/group.resource-type';
import { ResourceType } from '../../../../core/shared/resource-type';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { fadeInOut } from '../../../animations/fade';
export interface SearchEvent {
scope: string;
query: string
}
@Component({
selector: 'ds-eperson-group-list',
styleUrls: ['./eperson-group-list.component.scss'],
templateUrl: './eperson-group-list.component.html',
animations: [
fadeInOut
]
})
/**
* Component that shows a list of eperson or group
*/
export class EpersonGroupListComponent implements OnInit, OnDestroy {
/**
* A boolean representing id component should list eperson or group
*/
@Input() isListOfEPerson = true;
/**
* The uuid of eperson or group initially selected
*/
@Input() initSelected: string;
/**
* An event fired when a eperson or group is selected.
* Event's payload equals to DSpaceObject.
*/
@Output() select: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
/**
* Current search query
*/
public currentSearchQuery = '';
/**
* Current search scope
*/
public currentSearchScope = 'metadata';
/**
* Pagination config used to display the list
*/
public paginationOptions: PaginationComponentOptions = new PaginationComponentOptions();
/**
* The data service used to make request.
* It could be EPersonDataService or GroupDataService
*/
private dataService: DataService<DSpaceObject>;
/**
* A list of eperson or group
*/
private list$: BehaviorSubject<RemoteData<PaginatedList<DSpaceObject>>> = new BehaviorSubject<RemoteData<PaginatedList<DSpaceObject>>>({} as any);
/**
* The eperson or group's id selected
* @type {string}
*/
private entrySelectedId: BehaviorSubject<string> = new BehaviorSubject<string>('');
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
/**
* Initialize instance variables and inject the properly DataService
*
* @param {DSONameService} dsoNameService
* @param {Injector} parentInjector
*/
constructor(public dsoNameService: DSONameService, private parentInjector: Injector) {
}
/**
* Initialize the component
*/
ngOnInit(): void {
const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP;
const provider = getDataServiceFor(resourceType);
this.dataService = Injector.create({
providers: [],
parent: this.parentInjector
}).get(provider);
this.paginationOptions.id = uniqueId('eperson-group-list-pagination');
this.paginationOptions.pageSize = 5;
if (this.initSelected) {
this.entrySelectedId.next(this.initSelected);
}
this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery);
}
/**
* Method called when an entry is selected.
* Emit a new select Event
*
* @param entry The eperson or group selected
*/
emitSelect(entry: DSpaceObject): void {
this.select.emit(entry);
this.entrySelectedId.next(entry.id);
}
/**
* Return the list of eperson or group
*/
getList(): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
return this.list$.asObservable();
}
/**
* Return a boolean representing if a table row is selected
*
* @return {boolean}
*/
isSelected(entry: DSpaceObject): Observable<boolean> {
return this.entrySelectedId.asObservable().pipe(
map((selectedId) => isNotEmpty(selectedId) && selectedId === entry.id)
)
}
/**
* Method called on page change
*/
onPageChange(page: number): void {
this.paginationOptions.currentPage = page;
this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery);
}
/**
* Method called on search
*/
onSearch(searchEvent: SearchEvent) {
this.currentSearchQuery = searchEvent.query;
this.currentSearchScope = searchEvent.scope;
this.paginationOptions.currentPage = 1;
this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery);
}
/**
* Retrieve a paginate list of eperson or group
*/
updateList(config: PaginationComponentOptions, scope: string, query: string): void {
const options: FindListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: config.pageSize,
currentPage: config.currentPage
});
const search$: Observable<RemoteData<PaginatedList<DSpaceObject>>> = this.isListOfEPerson ?
(this.dataService as EPersonDataService).searchByScope(scope, query, options) :
(this.dataService as GroupDataService).searchGroups(query, options);
this.subs.push(search$.pipe(take(1))
.subscribe((list: RemoteData<PaginatedList<DSpaceObject>>) => {
this.list$.next(list)
})
);
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.list$ = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())
}
}

View File

@@ -0,0 +1,26 @@
<form class="d-flex justify-content-between"
[formGroup]="searchForm"
(ngSubmit)="submit(searchForm.value); $event.stopImmediatePropagation();" >
<div>
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
</select>
</div>
<div class="flex-grow-1 mr-3 ml-3">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
<span class="input-group-append">
<button type="submit" class="search-button btn btn-secondary">
{{ labelPrefix + 'search.button' | translate }}
</button>
</span>
</div>
</div>
<div>
<button type="button" class="search-button btn btn-secondary" (click)="submit(null); reset()">
{{ labelPrefix + 'button.see-all' | translate }}
</button>
</div>
</form>

View File

@@ -0,0 +1,115 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { createTestComponent } from '../../../../testing/utils.test';
import { EpersonSearchBoxComponent } from './eperson-search-box.component';
import { SearchEvent } from '../eperson-group-list.component';
describe('EpersonSearchBoxComponent test suite', () => {
let comp: EpersonSearchBoxComponent;
let compAsAny: any;
let fixture: ComponentFixture<EpersonSearchBoxComponent>;
let de;
let formBuilder: FormBuilder;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [
EpersonSearchBoxComponent,
TestComponent
],
providers: [
FormBuilder,
EpersonSearchBoxComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-group-search-box></ds-group-search-box>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create EpersonSearchBoxComponent', inject([EpersonSearchBoxComponent], (app: EpersonSearchBoxComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(EpersonSearchBoxComponent);
formBuilder = TestBed.get(FormBuilder);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should reset the form', () => {
comp.searchForm = formBuilder.group(({
query: 'test',
}));
comp.reset();
expect(comp.searchForm.controls.query.value).toBe('');
});
it('should emit new search event', () => {
const data = {
scope: 'metadata',
query: 'test'
}
const event: SearchEvent = {
scope: 'metadata',
query: 'test'
}
spyOn(comp.search, 'emit');
comp.submit(data);
expect(comp.search.emit).toHaveBeenCalledWith(event);
});
})
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
}

View File

@@ -0,0 +1,65 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Subscription } from 'rxjs';
import { SearchEvent } from '../eperson-group-list.component';
import { isNotNull } from '../../../../empty.util';
/**
* A component used to show a search box for epersons.
*/
@Component({
selector: 'ds-eperson-search-box',
templateUrl: './eperson-search-box.component.html',
})
export class EpersonSearchBoxComponent {
labelPrefix = 'admin.access-control.epeople.';
/**
* The search form
*/
searchForm;
/**
* List of subscriptions
*/
subs: Subscription[] = [];
/**
* An event fired when a search is triggred.
* Event's payload is a SearchEvent.
*/
@Output() search: EventEmitter<SearchEvent> = new EventEmitter<SearchEvent>();
constructor(private formBuilder: FormBuilder) {
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
}
/**
* Reset the search form
*/
reset() {
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
}
/**
* Emit a new search event
* @param data Form data
*/
submit(data: any) {
const event: SearchEvent = {
scope: isNotNull(data) ? data.scope : 'metadata',
query: isNotNull(data) ? data.query : ''
}
this.search.emit(event)
}
}

View File

@@ -0,0 +1,20 @@
<form class="d-flex justify-content-between"
[formGroup]="searchForm"
(ngSubmit)="submit(searchForm.value); $event.stopImmediatePropagation();" >
<div class="flex-grow-1 mr-3">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
<span class="input-group-append">
<button type="submit" class="search-button btn btn-secondary">
{{ labelPrefix + 'search.button' | translate }}
</button>
</span>
</div>
</div>
<div>
<button type="button" class="search-button btn btn-secondary" (click)="submit(null); reset()">
{{ labelPrefix + 'button.see-all' | translate }}
</button>
</div>
</form>

View File

@@ -0,0 +1,114 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { createTestComponent } from '../../../../testing/utils.test';
import { GroupSearchBoxComponent } from './group-search-box.component';
import { SearchEvent } from '../eperson-group-list.component';
describe('GroupSearchBoxComponent test suite', () => {
let comp: GroupSearchBoxComponent;
let compAsAny: any;
let fixture: ComponentFixture<GroupSearchBoxComponent>;
let de;
let formBuilder: FormBuilder;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [
GroupSearchBoxComponent,
TestComponent
],
providers: [
FormBuilder,
GroupSearchBoxComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-group-search-box></ds-group-search-box>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create GroupSearchBoxComponent', inject([GroupSearchBoxComponent], (app: GroupSearchBoxComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(GroupSearchBoxComponent);
formBuilder = TestBed.get(FormBuilder);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should reset the form', () => {
comp.searchForm = formBuilder.group(({
query: 'test',
}));
comp.reset();
expect(comp.searchForm.controls.query.value).toBe('');
});
it('should emit new search event', () => {
const data = {
query: 'test'
}
const event: SearchEvent = {
scope: '',
query: 'test'
}
spyOn(comp.search, 'emit');
comp.submit(data);
expect(comp.search.emit).toHaveBeenCalledWith(event);
});
})
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
}

View File

@@ -0,0 +1,61 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Subscription } from 'rxjs';
import { SearchEvent } from '../eperson-group-list.component';
/**
* A component used to show a search box for groups.
*/
@Component({
selector: 'ds-group-search-box',
templateUrl: './group-search-box.component.html',
})
export class GroupSearchBoxComponent {
labelPrefix = 'admin.access-control.groups.';
/**
* The search form
*/
searchForm;
/**
* List of subscriptions
*/
subs: Subscription[] = [];
/**
* An event fired when a search is triggred.
* Event's payload is a SearchEvent.
*/
@Output() search: EventEmitter<SearchEvent> = new EventEmitter<SearchEvent>();
constructor(private formBuilder: FormBuilder) {
this.searchForm = this.formBuilder.group(({
query: '',
}));
}
/**
* Reset the search form
*/
reset() {
this.searchForm = this.formBuilder.group(({
query: '',
}));
}
/**
* Emit a new search event
* @param data Form data
*/
submit(data: any) {
const event: SearchEvent = {
scope: '',
query: data.query
}
this.search.emit(event)
}
}

View File

@@ -0,0 +1,47 @@
<div>
<ds-form *ngIf="formModel"
#formRef="formComponent"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"></ds-form>
<div class="container-fluid">
<label for="ResourcePolicyObject">{{'resource-policies.form.eperson-group-list.label' | translate}}</label>
<input id="ResourcePolicyObject" class="form-control mb-3" type="text" readonly [value]="getResourcePolicyTargetName()">
<ngb-tabset *ngIf="canSetGrant()" type="pills">
<ngb-tab [title]="'resource-policies.form.eperson-group-list.tab.eperson' | translate">
<ng-template ngbTabContent>
<ds-eperson-group-list (select)="updateObjectSelected($event, true)"></ds-eperson-group-list>
</ng-template>
</ngb-tab>
<ngb-tab [title]="'resource-policies.form.eperson-group-list.tab.group' | translate">
<ng-template ngbTabContent>
<ds-eperson-group-list [isListOfEPerson]="false"
(select)="updateObjectSelected($event, false)"></ds-eperson-group-list>
</ng-template>
</ngb-tab>
</ngb-tabset>
<div>
<hr>
<div class="form-group row">
<div class="col text-right">
<button type="reset"
class="btn btn-default"
[disabled]="(isProcessing | async)"
(click)="onReset()">{{'form.cancel' | translate}}</button>
<button type="button"
class="btn btn-primary"
[disabled]="!(isFormValid() | async) || (isProcessing | async)"
(click)="onSubmit()">
<span *ngIf="(isProcessing | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}
</span>
<span *ngIf="!(isProcessing | async)">
{{'form.submit' | translate}}
</span>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,427 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { BrowserModule, By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { delay } from 'rxjs/operators';
import { TranslateModule } from '@ngx-translate/core';
import { createSuccessfulRemoteDataObject } from '../../remote-data.utils';
import { createTestComponent } from '../../testing/utils.test';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { RequestService } from '../../../core/data/request.service';
import { getMockRequestService } from '../../mocks/request.service.mock';
import { PolicyType } from '../../../core/resource-policy/models/policy-type.model';
import { ActionType } from '../../../core/resource-policy/models/action-type.model';
import { GroupMock } from '../../testing/group-mock';
import { ResourcePolicyEvent, ResourcePolicyFormComponent } from './resource-policy-form.component';
import { FormService } from '../../form/form.service';
import { getMockFormService } from '../../mocks/form-service.mock';
import { FormBuilderService } from '../../form/builder/form-builder.service';
import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component';
import { FormComponent } from '../../form/form.component';
import { stringToNgbDateStruct } from '../../date.util';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type';
import { EPersonMock } from '../../testing/eperson.mock';
export const mockResourcePolicyFormData = {
name: [
{
value: 'name',
language: null,
authority: null,
display: 'name',
confidence: -1,
place: 0,
otherInformation: null
}
],
description: [
{
value: 'description',
language: null,
authority: null,
display: 'description',
confidence: -1,
place: 0,
otherInformation: null
}
],
policyType: [
{
value: 'TYPE_WORKFLOW',
language: null,
authority: null,
display: 'TYPE_WORKFLOW',
confidence: -1,
place: 0,
otherInformation: null
}
],
action: [
{
value: 'WRITE',
language: null,
authority: null,
display: 'WRITE',
confidence: -1,
place: 0,
otherInformation: null
}
],
date: {
start: [
{
value: { year: '2019', month: '04', day: '14' },
language: null,
authority: null,
display: '2019-04-14',
confidence: -1,
place: 0,
otherInformation: null
}
],
end: [
{
value: { year: '2020', month: '04', day: '14' },
language: null,
authority: null,
display: '2020-04-14',
confidence: -1,
place: 0,
otherInformation: null
}
],
}
};
export const submittedResourcePolicy = Object.assign(new ResourcePolicy(), {
name: 'name',
description: 'description',
policyType: PolicyType.TYPE_WORKFLOW,
action: ActionType.WRITE,
startDate: '2019-04-14T00:00:00Z',
endDate: '2020-04-14T00:00:00Z',
type: RESOURCE_POLICY
});
describe('ResourcePolicyFormComponent test suite', () => {
let comp: ResourcePolicyFormComponent;
let compAsAny: any;
let fixture: ComponentFixture<ResourcePolicyFormComponent>;
let de;
let scheduler: TestScheduler;
const resourcePolicy: any = {
id: '1',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.READ,
startDate: '2019-04-14',
endDate: '2020-04-14',
type: 'resourcepolicy',
uuid: 'resource-policy-1',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
},
eperson: observableOf(createSuccessfulRemoteDataObject({})),
group: observableOf(createSuccessfulRemoteDataObject(GroupMock))
};
const epersonService = jasmine.createSpyObj('epersonService', {
findByHref: jasmine.createSpy('findByHref'),
findAll: jasmine.createSpy('findAll')
});
const groupService = jasmine.createSpyObj('groupService', {
findByHref: jasmine.createSpy('findByHref'),
findAll: jasmine.createSpy('findAll')
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [
FormComponent,
EpersonGroupListComponent,
ResourcePolicyFormComponent,
TestComponent
],
providers: [
{ provide: EPersonDataService, useValue: epersonService },
{ provide: FormService, useValue: getMockFormService() },
{ provide: GroupDataService, useValue: groupService },
{ provide: RequestService, useValue: getMockRequestService() },
FormBuilderService,
ChangeDetectorRef,
ResourcePolicyFormComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-resource-policy-form [resourcePolicy]="resourcePolicy" [isProcessing]="isProcessing"></ds-resource-policy-form>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create ResourcePolicyFormComponent', inject([ResourcePolicyFormComponent], (app: ResourcePolicyFormComponent) => {
expect(app).toBeDefined();
}));
});
describe('when resource policy is not provided', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(ResourcePolicyFormComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
comp.isProcessing = observableOf(false);
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should init form model properly', () => {
spyOn(compAsAny, 'isFormValid').and.returnValue(observableOf(false));
spyOn(compAsAny, 'initModelsValue').and.callThrough();
spyOn(compAsAny, 'buildResourcePolicyForm').and.callThrough();
fixture.detectChanges();
expect(compAsAny.buildResourcePolicyForm).toHaveBeenCalled();
expect(compAsAny.initModelsValue).toHaveBeenCalled();
expect(compAsAny.formModel.length).toBe(5);
expect(compAsAny.subs.length).toBe(0);
});
it('should can set grant', () => {
expect(comp.canSetGrant()).toBeTruthy();
});
it('should not have a target name', () => {
expect(comp.getResourcePolicyTargetName()).toBe('');
});
it('should emit reset event', () => {
spyOn(compAsAny.reset, 'emit');
comp.onReset();
expect(compAsAny.reset.emit).toHaveBeenCalled();
});
it('should update resource policy grant object properly', () => {
comp.updateObjectSelected(EPersonMock, true);
expect(comp.resourcePolicyGrant).toEqual(EPersonMock);
expect(comp.resourcePolicyGrantType).toBe('eperson');
comp.updateObjectSelected(GroupMock, false);
expect(comp.resourcePolicyGrant).toEqual(GroupMock);
expect(comp.resourcePolicyGrantType).toBe('group');
});
});
describe('when resource policy is provided', () => {
beforeEach(() => {
// initTestScheduler();
fixture = TestBed.createComponent(ResourcePolicyFormComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
comp.resourcePolicy = resourcePolicy;
comp.isProcessing = observableOf(false);
compAsAny.ePersonService.findByHref.and.returnValue(
observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100))
);
compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock)));
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should init form model properly', () => {
spyOn(compAsAny, 'isFormValid').and.returnValue(observableOf(false));
spyOn(compAsAny, 'initModelsValue').and.callThrough();
spyOn(compAsAny, 'buildResourcePolicyForm').and.callThrough();
fixture.detectChanges();
expect(compAsAny.buildResourcePolicyForm).toHaveBeenCalled();
expect(compAsAny.initModelsValue).toHaveBeenCalled();
expect(compAsAny.formModel.length).toBe(5);
expect(compAsAny.subs.length).toBe(1);
expect(compAsAny.formModel[2].value).toBe('TYPE_SUBMISSION');
expect(compAsAny.formModel[3].value).toBe('READ');
expect(compAsAny.formModel[4].get(0).value).toEqual(stringToNgbDateStruct('2019-04-14'));
expect(compAsAny.formModel[4].get(1).value).toEqual(stringToNgbDateStruct('2020-04-14'));
});
it('should init resourcePolicyGrant properly', () => {
compAsAny.isActive = true;
scheduler = getTestScheduler();
scheduler.schedule(() => comp.ngOnInit());
scheduler.flush();
expect(compAsAny.resourcePolicyGrant).toEqual(GroupMock);
});
it('should not can set grant', () => {
expect(comp.canSetGrant()).toBeFalsy();
});
it('should have a target name', () => {
compAsAny.resourcePolicyGrant = GroupMock;
expect(comp.getResourcePolicyTargetName()).toBe('testgroupname');
});
});
describe('when form is valid', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ResourcePolicyFormComponent);
comp = fixture.componentInstance;
compAsAny = comp;
comp.resourcePolicy = resourcePolicy;
comp.isProcessing = observableOf(false);
compAsAny.ePersonService.findByHref.and.returnValue(
observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100))
);
compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock)));
compAsAny.formService.isValid.and.returnValue(observableOf(true));
compAsAny.isActive = true;
comp.resourcePolicyGrant = GroupMock;
comp.resourcePolicyGrantType = 'group';
fixture.detectChanges();
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should not have submit button disabled when submission is valid', () => {
const depositBtn: any = fixture.debugElement.query(By.css('.btn-primary'));
expect(depositBtn.nativeElement.disabled).toBeFalsy();
});
it('should emit submit event', () => {
spyOn(compAsAny.submit, 'emit');
spyOn(compAsAny, 'createResourcePolicyByFormData').and.callThrough();
compAsAny.formService.getFormData.and.returnValue(observableOf(mockResourcePolicyFormData));
const eventPayload: ResourcePolicyEvent = Object.create({});
eventPayload.object = submittedResourcePolicy;
eventPayload.target = {
type: 'group',
uuid: GroupMock.id
};
scheduler = getTestScheduler();
scheduler.schedule(() => comp.onSubmit());
scheduler.flush();
expect(compAsAny.submit.emit).toHaveBeenCalledWith(eventPayload);
expect(compAsAny.createResourcePolicyByFormData).toHaveBeenCalled();
});
});
describe('when form is not valid', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ResourcePolicyFormComponent);
comp = fixture.componentInstance;
compAsAny = comp;
comp.resourcePolicy = resourcePolicy;
comp.isProcessing = observableOf(false);
compAsAny.ePersonService.findByHref.and.returnValue(
observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100))
);
compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock)));
compAsAny.formService.isValid.and.returnValue(observableOf(false));
compAsAny.isActive = true;
fixture.detectChanges();
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should have submit button disabled when submission is valid', () => {
const depositBtn: any = fixture.debugElement.query(By.css('.btn-primary'));
expect(depositBtn.nativeElement.disabled).toBeTruthy();
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
resourcePolicy = null;
isProcessing = observableOf(false);
}

View File

@@ -0,0 +1,317 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Observable, of as observableOf, race as observableRace } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import {
DynamicDatePickerModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicSelectModel
} from '@ng-dynamic-forms/core';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { DsDynamicInputModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
import {
RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG,
RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG,
RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT,
RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG,
RESOURCE_POLICY_FORM_END_DATE_CONFIG,
RESOURCE_POLICY_FORM_END_DATE_LAYOUT,
RESOURCE_POLICY_FORM_NAME_CONFIG,
RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG,
RESOURCE_POLICY_FORM_START_DATE_CONFIG,
RESOURCE_POLICY_FORM_START_DATE_LAYOUT
} from './resource-policy-form.model';
import { DsDynamicTextAreaModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
import { FormService } from '../../form/form.service';
import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type';
import { RemoteData } from '../../../core/data/remote-data';
import { Subscription } from 'rxjs/internal/Subscription';
import { dateToISOFormat, stringToNgbDateStruct } from '../../date.util';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { RequestService } from '../../../core/data/request.service';
export interface ResourcePolicyEvent {
object: ResourcePolicy,
target: {
type: string,
uuid: string
}
}
@Component({
selector: 'ds-resource-policy-form',
templateUrl: './resource-policy-form.component.html',
})
/**
* Component that show form for adding/editing a resource policy
*/
export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
/**
* If given contains the resource policy to edit
* @type {ResourcePolicy}
*/
@Input() resourcePolicy: ResourcePolicy;
/**
* A boolean representing if form submit operation is processing
* @type {boolean}
*/
@Input() isProcessing: Observable<boolean> = observableOf(false);
/**
* An event fired when form is canceled.
* Event's payload is empty.
*/
@Output() reset: EventEmitter<any> = new EventEmitter<any>();
/**
* An event fired when form is submitted.
* Event's payload equals to a new ResourcePolicy.
*/
@Output() submit: EventEmitter<ResourcePolicyEvent> = new EventEmitter<ResourcePolicyEvent>();
/**
* The form id
* @type {string}
*/
public formId: string;
/**
* The form model
* @type {DynamicFormControlModel[]}
*/
public formModel: DynamicFormControlModel[];
/**
* The eperson or group that will be grant of the permission
* @type {DSpaceObject}
*/
public resourcePolicyGrant: DSpaceObject;
/**
* The type of the object that will be grant of the permission. It could be 'eperson' or 'group'
* @type {string}
*/
public resourcePolicyGrantType: string;
/**
* A boolean representing if component is active
* @type {boolean}
*/
private isActive: boolean;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {DSONameService} dsoNameService
* @param {EPersonDataService} ePersonService
* @param {FormService} formService
* @param {GroupDataService} groupService
* @param {RequestService} requestService
*/
constructor(
private dsoNameService: DSONameService,
private ePersonService: EPersonDataService,
private formService: FormService,
private groupService: GroupDataService,
private requestService: RequestService,
) {
}
/**
* Initialize the component, setting up the form model
*/
ngOnInit(): void {
this.isActive = true;
this.formId = this.formService.getUniqueId('resource-policy-form');
this.formModel = this.buildResourcePolicyForm();
if (!this.canSetGrant()) {
this.requestService.removeByHrefSubstring(this.resourcePolicy._links.eperson.href);
this.requestService.removeByHrefSubstring(this.resourcePolicy._links.group.href);
const epersonRD$ = this.ePersonService.findByHref(this.resourcePolicy._links.eperson.href).pipe(
getSucceededRemoteData()
);
const groupRD$ = this.groupService.findByHref(this.resourcePolicy._links.group.href).pipe(
getSucceededRemoteData()
);
const dsoRD$: Observable<RemoteData<DSpaceObject>> = observableRace(epersonRD$, groupRD$);
this.subs.push(
dsoRD$.pipe(
filter(() => this.isActive),
).subscribe((dsoRD: RemoteData<DSpaceObject>) => {
this.resourcePolicyGrant = dsoRD.payload;
})
)
}
}
/**
* Method to check if the form status is valid or not
*
* @return Observable that emits the form status
*/
isFormValid(): Observable<boolean> {
return this.formService.isValid(this.formId).pipe(
map((isValid: boolean) => isValid && isNotEmpty(this.resourcePolicyGrant))
)
}
/**
* Initialize the form model
*
* @return the form models
*/
private buildResourcePolicyForm(): DynamicFormControlModel[] {
const formModel: DynamicFormControlModel[] = [];
// TODO to be removed when https://jira.lyrasis.org/browse/DS-4477 will be implemented
const policyTypeConf = Object.assign({}, RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG, {
disabled: isNotEmpty(this.resourcePolicy)
});
// TODO to be removed when https://jira.lyrasis.org/browse/DS-4477 will be implemented
const actionConf = Object.assign({}, RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG, {
disabled: isNotEmpty(this.resourcePolicy)
});
formModel.push(
new DsDynamicInputModel(RESOURCE_POLICY_FORM_NAME_CONFIG),
new DsDynamicTextAreaModel(RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG),
new DynamicSelectModel(policyTypeConf),
new DynamicSelectModel(actionConf)
);
const startDateModel = new DynamicDatePickerModel(
RESOURCE_POLICY_FORM_START_DATE_CONFIG,
RESOURCE_POLICY_FORM_START_DATE_LAYOUT
);
const endDateModel = new DynamicDatePickerModel(
RESOURCE_POLICY_FORM_END_DATE_CONFIG,
RESOURCE_POLICY_FORM_END_DATE_LAYOUT
);
const dateGroupConfig = Object.assign({}, RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG, { group: [] });
dateGroupConfig.group.push(startDateModel, endDateModel);
formModel.push(new DynamicFormGroupModel(dateGroupConfig, RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT));
this.initModelsValue(formModel);
return formModel
}
/**
* Setting up the form models value
*
* @return the form models
*/
initModelsValue(formModel: DynamicFormControlModel[]): DynamicFormControlModel[] {
if (this.resourcePolicy) {
formModel.forEach((model: any) => {
if (model.id === 'date') {
if (hasValue(this.resourcePolicy.startDate)) {
model.get(0).valueUpdates.next(stringToNgbDateStruct(this.resourcePolicy.startDate));
}
if (hasValue(this.resourcePolicy.endDate)) {
model.get(1).valueUpdates.next(stringToNgbDateStruct(this.resourcePolicy.endDate));
}
} else {
if (this.resourcePolicy.hasOwnProperty(model.id) && this.resourcePolicy[model.id]) {
model.valueUpdates.next(this.resourcePolicy[model.id]);
}
}
})
}
return formModel;
}
/**
* Return a boolean representing If is possible to set policy grant
*
* @return true if is possible, false otherwise
*/
canSetGrant(): boolean {
return isEmpty(this.resourcePolicy);
}
/**
* Return the name of the eperson or group that will be grant of the permission
*
* @return the object name
*/
getResourcePolicyTargetName(): string {
return isNotEmpty(this.resourcePolicyGrant) ? this.dsoNameService.getName(this.resourcePolicyGrant) : '';
}
/**
* Update reference to the eperson or group that will be grant of the permission
*/
updateObjectSelected(object: DSpaceObject, isEPerson: boolean): void {
this.resourcePolicyGrant = object;
this.resourcePolicyGrantType = isEPerson ? 'eperson' : 'group';
}
/**
* Method called on reset
* Emit a new reset Event
*/
onReset(): void {
this.reset.emit();
}
/**
* Method called on submit.
* Emit a new submit Event whether the form is valid
*/
onSubmit(): void {
this.formService.getFormData(this.formId).pipe(take(1))
.subscribe((data) => {
const eventPayload: ResourcePolicyEvent = Object.create({});
eventPayload.object = this.createResourcePolicyByFormData(data);
eventPayload.target = {
type: this.resourcePolicyGrantType,
uuid: this.resourcePolicyGrant.id
};
this.submit.emit(eventPayload);
})
}
/**
* Create e new ResourcePolicy by form data
*
* @return the new ResourcePolicy object
*/
createResourcePolicyByFormData(data): ResourcePolicy {
const resourcePolicy = new ResourcePolicy();
resourcePolicy.name = (data.name) ? data.name[0].value : null;
resourcePolicy.description = (data.description) ? data.description[0].value : null;
resourcePolicy.policyType = (data.policyType) ? data.policyType[0].value : null;
resourcePolicy.action = (data.action) ? data.action[0].value : null;
resourcePolicy.startDate = (data.date && data.date.start) ? dateToISOFormat(data.date.start[0].value) : null;
resourcePolicy.endDate = (data.date && data.date.end) ? dateToISOFormat(data.date.end[0].value) : null;
resourcePolicy.type = RESOURCE_POLICY;
return resourcePolicy;
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.isActive = false;
this.formModel = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())
}
}

View File

@@ -0,0 +1,154 @@
import {
DynamicDatePickerModelConfig,
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicFormOptionConfig,
DynamicSelectModelConfig,
} from '@ng-dynamic-forms/core';
import { DsDynamicInputModelConfig } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { DsDynamicTextAreaModelConfig } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { PolicyType } from '../../../core/resource-policy/models/policy-type.model';
import { ActionType } from '../../../core/resource-policy/models/action-type.model';
const policyTypeList: Array<DynamicFormOptionConfig<any>> = [
{
label: PolicyType.TYPE_SUBMISSION,
value: PolicyType.TYPE_SUBMISSION
},
{
label: PolicyType.TYPE_WORKFLOW,
value: PolicyType.TYPE_WORKFLOW
},
{
label: PolicyType.TYPE_INHERITED,
value: PolicyType.TYPE_INHERITED
},
{
label: PolicyType.TYPE_CUSTOM,
value: PolicyType.TYPE_CUSTOM
},
];
const policyActionList: Array<DynamicFormOptionConfig<any>> = [
{
label: ActionType.READ.toString(),
value: ActionType.READ
},
{
label: ActionType.WRITE.toString(),
value: ActionType.WRITE
},
{
label: ActionType.REMOVE.toString(),
value: ActionType.REMOVE
},
{
label: ActionType.ADMIN.toString(),
value: ActionType.ADMIN
},
{
label: ActionType.DELETE.toString(),
value: ActionType.DELETE
},
{
label: ActionType.WITHDRAWN_READ.toString(),
value: ActionType.WITHDRAWN_READ
},
{
label: ActionType.DEFAULT_BITSTREAM_READ.toString(),
value: ActionType.DEFAULT_BITSTREAM_READ
},
{
label: ActionType.DEFAULT_ITEM_READ.toString(),
value: ActionType.DEFAULT_ITEM_READ
}
];
export const RESOURCE_POLICY_FORM_NAME_CONFIG: DsDynamicInputModelConfig = {
id: 'name',
label: 'resource-policies.form.name.label',
metadataFields: [],
repeatable: false,
submissionId: ''
};
export const RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG: DsDynamicTextAreaModelConfig = {
id: 'description',
label: 'resource-policies.form.description.label',
metadataFields: [],
repeatable: false,
rows: 10,
submissionId: ''
};
export const RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG: DynamicSelectModelConfig<any> = {
id: 'policyType',
label: 'resource-policies.form.policy-type.label',
options: policyTypeList,
required: true,
validators: {
required: null
},
errorMessages: {
required: 'resource-policies.form.policy-type.required'
}
};
export const RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG: DynamicSelectModelConfig<any> = {
id: 'action',
label: 'resource-policies.form.action-type.label',
options: policyActionList,
required: true,
validators: {
required: null
},
errorMessages: {
required: 'resource-policies.form.action-type.required'
}
};
export const RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG: DynamicFormGroupModelConfig = {
id: 'date',
group: []
};
export const RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT: DynamicFormControlLayout = {
element: {
control: 'form-row',
}
};
export const RESOURCE_POLICY_FORM_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'start',
label: 'resource-policies.form.date.start.label',
placeholder: 'resource-policies.form.date.start.label',
inline: false,
toggleIcon: 'far fa-calendar-alt'
};
export const RESOURCE_POLICY_FORM_START_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
container: 'p-0',
label: 'col-form-label'
},
grid: {
host: 'col-md-6'
}
};
export const RESOURCE_POLICY_FORM_END_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'end',
label: 'resource-policies.form.date.end.label',
placeholder: 'resource-policies.form.date.end.label',
inline: false,
toggleIcon: 'far fa-calendar-alt'
};
export const RESOURCE_POLICY_FORM_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: {
container: 'p-0',
label: 'col-form-label'
},
grid: {
host: 'col-md-6'
}
};

View File

@@ -0,0 +1,53 @@
import { Injectable, Injector } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { find } from 'rxjs/operators';
import { getDataServiceFor } from '../../../core/cache/builders/build-decorators';
import { ResourceType } from '../../../core/shared/resource-type';
import { DataService } from '../../../core/data/data.service';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { hasValue, isEmpty } from '../../empty.util';
import { RemoteData } from '../../../core/data/remote-data';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable()
export class ResourcePolicyTargetResolver implements Resolve<RemoteData<DSpaceObject>> {
/**
* The data service used to make request.
*/
private dataService: DataService<DSpaceObject>;
constructor(private parentInjector: Injector, private router: Router) {
}
/**
* Method for resolving an item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<DSpaceObject>> {
const targetType = route.queryParamMap.get('targetType');
const policyTargetId = route.queryParamMap.get('policyTargetId');
if (isEmpty(targetType) || isEmpty(policyTargetId)) {
this.router.navigateByUrl('/404', { skipLocationChange: true });
}
const provider = getDataServiceFor(new ResourceType(targetType));
this.dataService = Injector.create({
providers: [],
parent: this.parentInjector
}).get(provider);
return this.dataService.findById(policyTargetId).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
);
}
}

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { find } from 'rxjs/operators';
import { hasValue, isEmpty } from '../../empty.util';
import { RemoteData } from '../../../core/data/remote-data';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { followLink } from '../../utils/follow-link-config.model';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable()
export class ResourcePolicyResolver implements Resolve<RemoteData<ResourcePolicy>> {
constructor(private resourcePolicyService: ResourcePolicyService, private router: Router) {
}
/**
* Method for resolving an item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<ResourcePolicy>> {
const policyId = route.queryParamMap.get('policyId');
if (isEmpty(policyId)) {
this.router.navigateByUrl('/404', { skipLocationChange: true });
}
return this.resourcePolicyService.findById(policyId, followLink('eperson'), followLink('group')).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
);
}
}

View File

@@ -0,0 +1,99 @@
<div *ngIf="(getResourcePolicies() | async)?.length > 0" class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th colspan="10">
<div class="d-flex justify-content-between align-items-center m-0">
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}}
<div>
<button class="btn btn-danger float-right ml-1"
[disabled]="(!(canDelete() | async)) || (isProcessingDelete() | async)"
[title]="'resource-policies.delete.btn.title' | translate"
(click)="deleteSelectedResourcePolicies()">
<span *ngIf="(isProcessingDelete() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}
</span>
<span *ngIf="!(isProcessingDelete() | async)">
<i class='fas fa-trash-alt fa-fw'></i>
{{'resource-policies.delete.btn' | translate}}
</span>
</button>
<button class="btn btn-success float-right"
[disabled]="(isProcessingDelete() | async)"
[title]="'resource-policies.add.for.' + resourceType | translate"
(click)="redirectToResourcePolicyCreatePage()">
<i class='fas fa-plus'></i>
{{'resource-policies.add.button' | translate}}
</button>
</div>
</div>
</th>
</tr>
<tr class="text-center">
<th>
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
[id]="'selectAll_' + resourceUUID"
(change)="selectAllCheckbox($event)">
<label class="custom-control-label" [for]="'selectAll_' + resourceUUID"></label>
</div>
</th>
<th>{{'resource-policies.table.headers.id' | translate}}</th>
<th>{{'resource-policies.table.headers.name' | translate}}</th>
<th>{{'resource-policies.table.headers.policyType' | translate}}</th>
<th>{{'resource-policies.table.headers.action' | translate}}</th>
<th>{{'resource-policies.table.headers.eperson' | translate}}</th>
<th>{{'resource-policies.table.headers.group' | translate}}</th>
<th>{{'resource-policies.table.headers.date.start' | translate}}</th>
<th>{{'resource-policies.table.headers.date.end' | translate}}</th>
<th>{{'resource-policies.table.headers.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let entry of (getResourcePolicies() | async); trackById">
<td class="text-center">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
[id]="entry.id"
[ngModel]="entry.checked"
(ngModelChange)="selectCheckbox(entry, $event)">
<label class="custom-control-label" [for]="entry.id"></label>
</div>
</td>
<th scope="row">
{{entry.id}}
</th>
<td>{{entry.policy.name}}</td>
<td>{{entry.policy.policyType}}</td>
<td>{{entry.policy.action}}</td>
<td *ngIf="(hasEPerson(entry.policy) | async)">
{{getEPersonName(entry.policy) | async}}
</td>
<td *ngIf="!(hasEPerson(entry.policy) | async)"></td>
<td *ngIf="(hasGroup(entry.policy) | async)">
{{getGroupName(entry.policy) | async}}
</td>
<td *ngIf="!(hasGroup(entry.policy) | async)"></td>
<td>{{formatDate(entry.policy.startDate)}}</td>
<td>{{formatDate(entry.policy.endDate)}}</td>
<td class="text-center">
<div class="btn-group edit-field">
<button class="btn btn-outline-primary btn-sm"
[title]="'resource-policies.table.headers.edit.policy' | translate"
(click)="redirectToResourcePolicyEditPage(entry.policy)">
<i class="fas fa-edit fa-fw"></i>
</button>
<button *ngIf="(hasGroup(entry.policy) | async)" class="btn btn-outline-primary btn-sm"
[title]="'resource-policies.table.headers.edit.group' | translate"
(click)="redirectToGroupEditPage(entry.policy)">
<i class="fas fa-users fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,3 @@
td .btn-link:focus {
box-shadow: none !important;
}

View File

@@ -0,0 +1,485 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { TranslateModule } from '@ngx-translate/core';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { Bitstream } from '../../core/shared/bitstream.model';
import { Bundle } from '../../core/shared/bundle.model';
import { createMockRDPaginatedObs } from '../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec';
import { Item } from '../../core/shared/item.model';
import { LinkService } from '../../core/cache/builders/link.service';
import { getMockLinkService } from '../mocks/link-service.mock';
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
import { createTestComponent } from '../testing/utils.test';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { NotificationsService } from '../notifications/notifications.service';
import { NotificationsServiceStub } from '../testing/notifications-service.stub';
import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service';
import { getMockResourcePolicyService } from '../mocks/mock-resource-policy-service';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { RequestService } from '../../core/data/request.service';
import { getMockRequestService } from '../mocks/request.service.mock';
import { RouterStub } from '../testing/router.stub';
import { PaginatedList } from '../../core/data/paginated-list';
import { PageInfo } from '../../core/shared/page-info.model';
import { ResourcePoliciesComponent } from './resource-policies.component';
import { PolicyType } from '../../core/resource-policy/models/policy-type.model';
import { ActionType } from '../../core/resource-policy/models/action-type.model';
import { EPersonMock } from '../testing/eperson.mock';
import { GroupMock } from '../testing/group-mock';
describe('ResourcePoliciesComponent test suite', () => {
let comp: ResourcePoliciesComponent;
let compAsAny: any;
let fixture: ComponentFixture<ResourcePoliciesComponent>;
let de;
let routerStub: any;
let scheduler: TestScheduler;
const notificationsServiceStub = new NotificationsServiceStub();
const resourcePolicyService: any = getMockResourcePolicyService();
const linkService: any = getMockLinkService();
const resourcePolicy: any = {
id: '1',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.READ,
startDate: null,
endDate: null,
type: 'resourcepolicy',
uuid: 'resource-policy-1',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
},
eperson: observableOf(createSuccessfulRemoteDataObject({})),
group: observableOf(createSuccessfulRemoteDataObject(GroupMock))
};
const anotherResourcePolicy: any = {
id: '2',
name: null,
description: null,
policyType: PolicyType.TYPE_SUBMISSION,
action: ActionType.WRITE,
startDate: null,
endDate: null,
type: 'resourcepolicy',
uuid: 'resource-policy-2',
_links: {
eperson: {
href: 'https://rest.api/rest/api/eperson'
},
group: {
href: 'https://rest.api/rest/api/group'
},
self: {
href: 'https://rest.api/rest/api/resourcepolicies/1'
},
},
eperson: observableOf(createSuccessfulRemoteDataObject(EPersonMock)),
group: observableOf(createSuccessfulRemoteDataObject({}))
};
const bitstream1 = Object.assign(new Bitstream(), {
id: 'bitstream1',
uuid: 'bitstream1'
});
const bitstream2 = Object.assign(new Bitstream(), {
id: 'bitstream2',
uuid: 'bitstream2'
});
const bitstream3 = Object.assign(new Bitstream(), {
id: 'bitstream3',
uuid: 'bitstream3'
});
const bitstream4 = Object.assign(new Bitstream(), {
id: 'bitstream4',
uuid: 'bitstream4'
});
const bundle1 = Object.assign(new Bundle(), {
id: 'bundle1',
uuid: 'bundle1',
_links: {
self: { href: 'bundle1-selflink' }
},
bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2])
});
const bundle2 = Object.assign(new Bundle(), {
id: 'bundle2',
uuid: 'bundle2',
_links: {
self: { href: 'bundle2-selflink' }
},
bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4])
});
const item = Object.assign(new Item(), {
uuid: 'itemUUID',
id: 'itemUUID',
_links: {
self: { href: 'item-selflink' }
},
bundles: createMockRDPaginatedObs([bundle1, bundle2])
});
const routeStub = {
data: observableOf({
item: createSuccessfulRemoteDataObject(item)
})
};
const epersonService = jasmine.createSpyObj('epersonService', {
findByHref: jasmine.createSpy('findByHref'),
});
const groupService = jasmine.createSpyObj('groupService', {
findByHref: jasmine.createSpy('findByHref'),
});
routerStub = Object.assign(new RouterStub(), {
url: `url/edit`
});
const getInitEntries = () => {
return [
Object.assign({}, {
id: resourcePolicy.id,
policy: resourcePolicy,
checked: false
}),
Object.assign({}, {
id: anotherResourcePolicy.id,
policy: anotherResourcePolicy,
checked: false
})
]
}
const resourcePolicySelectedEntries = [
{
id: resourcePolicy.id,
policy: resourcePolicy,
checked: true
},
{
id: anotherResourcePolicy.id,
policy: anotherResourcePolicy,
checked: false
}
];
const pageInfo = new PageInfo();
const array = [resourcePolicy, anotherResourcePolicy];
const paginatedList = new PaginatedList(pageInfo, array);
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [
ResourcePoliciesComponent,
TestComponent
],
providers: [
{ provide: LinkService, useValue: linkService },
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: EPersonDataService, useValue: epersonService },
{ provide: GroupDataService, useValue: groupService },
{ provide: NotificationsService, useValue: notificationsServiceStub },
{ provide: ResourcePolicyService, useValue: resourcePolicyService },
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: Router, useValue: routerStub },
ChangeDetectorRef,
ResourcePoliciesComponent
], schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-resource-policies [resourceUUID]="resourceUUID" [resourceType]="resourceType"></ds-resource-policies>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create ResourcePoliciesComponent', inject([ResourcePoliciesComponent], (app: ResourcePoliciesComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ResourcePoliciesComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
linkService.resolveLink.and.callFake((object, link) => object);
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should init component properly', () => {
spyOn(comp, 'initResourcePolicyLIst');
fixture.detectChanges();
expect(compAsAny.isActive).toBeTruthy();
expect(comp.initResourcePolicyLIst).toHaveBeenCalled();
});
it('should init resource policies list properly', () => {
const expected = getInitEntries();
compAsAny.isActive = true;
resourcePolicyService.searchByResource.and.returnValue(hot('a|', {
a: paginatedListRD
}));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.initResourcePolicyLIst());
scheduler.flush();
expect(compAsAny.resourcePoliciesEntries$.value).toEqual(expected);
});
});
describe('', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ResourcePoliciesComponent);
comp = fixture.componentInstance;
compAsAny = fixture.componentInstance;
linkService.resolveLink.and.callFake((object, link) => object);
compAsAny.isActive = true;
const initResourcePolicyEntries = getInitEntries();
compAsAny.resourcePoliciesEntries$.next(initResourcePolicyEntries);
resourcePolicyService.searchByResource.and.returnValue(observableOf({}));
spyOn(comp, 'initResourcePolicyLIst').and.callFake(() => ({}));
fixture.detectChanges();
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
describe('canDelete', () => {
beforeEach(() => {
const initResourcePolicyEntries = getInitEntries();
compAsAny.resourcePoliciesEntries$.next(initResourcePolicyEntries);
fixture.detectChanges();
});
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
it('should return false when no row is selected', () => {
expect(comp.canDelete()).toBeObservable(cold('(a|)', {
a: false
}));
});
it('should return true when al least is selected', () => {
const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input'));
const event = { target: { checked: true } };
checkbox.triggerEventHandler('change', event);
expect(comp.canDelete()).toBeObservable(cold('(a|)', {
a: true
}));
});
});
it('should render a table with a row for each policy', () => {
const rows = fixture.debugElement.queryAll(By.css('table > tbody > tr'));
expect(rows.length).toBe(2);
});
describe('deleteSelectedResourcePolicies', () => {
beforeEach(() => {
compAsAny.resourcePoliciesEntries$.next(resourcePolicySelectedEntries);
fixture.detectChanges();
});
it('should notify success when delete is successful', () => {
resourcePolicyService.delete.and.returnValue(observableOf(true));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.deleteSelectedResourcePolicies());
scheduler.flush();
expect(notificationsServiceStub.success).toHaveBeenCalled();
expect(comp.initResourcePolicyLIst).toHaveBeenCalled();
});
it('should notify error when delete is not successful', () => {
resourcePolicyService.delete.and.returnValue(observableOf(false));
scheduler = getTestScheduler();
scheduler.schedule(() => comp.deleteSelectedResourcePolicies());
scheduler.flush();
expect(notificationsServiceStub.error).toHaveBeenCalled();
expect(comp.initResourcePolicyLIst).toHaveBeenCalled();
});
});
it('should get the resource\'s policy list', () => {
const initResourcePolicyEntries = getInitEntries();
expect(comp.getResourcePolicies()).toBeObservable(cold('a', {
a: initResourcePolicyEntries
}));
});
describe('hasEPerson', () => {
it('should true when policy is link to the eperson', () => {
expect(comp.hasEPerson(anotherResourcePolicy)).toBeObservable(cold('(ab|)', {
a: false,
b: true
}));
});
it('should false when policy is not link to the eperson', () => {
expect(comp.hasEPerson(resourcePolicy)).toBeObservable(cold('(aa|)', {
a: false
}));
});
});
describe('hasGroup', () => {
it('should true when policy is link to the group', () => {
expect(comp.hasGroup(resourcePolicy)).toBeObservable(cold('(ab|)', {
a: false,
b: true
}));
});
it('should false when policy is not link to the group', () => {
expect(comp.hasGroup(anotherResourcePolicy)).toBeObservable(cold('(aa|)', {
a: false
}));
});
});
describe('getEPersonName', () => {
it('should return the eperson name', () => {
expect(comp.getEPersonName(anotherResourcePolicy)).toBeObservable(cold('(ab|)', {
a: '',
b: 'User Test'
}));
});
});
describe('getGroupName', () => {
it('should return the group name', () => {
expect(comp.getGroupName(resourcePolicy)).toBeObservable(cold('(ab|)', {
a: '',
b: 'testgroupname'
}));
});
});
it('should format date properly', () => {
expect(comp.formatDate('2020-04-14T12:00:00Z')).toBe('2020-04-14');
});
it('should select All Checkbox', () => {
spyOn(comp, 'selectAllCheckbox').and.callThrough();
const checkbox = fixture.debugElement.query(By.css('table > thead > tr:nth-child(2) input'));
const event = { target: { checked: true } };
checkbox.triggerEventHandler('change', event);
expect(comp.selectAllCheckbox).toHaveBeenCalled();
});
it('should select a Checkbox', () => {
spyOn(comp, 'selectCheckbox').and.callThrough();
const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input'));
const event = { target: { checked: true } };
checkbox.triggerEventHandler('change', event);
expect(comp.selectCheckbox).toHaveBeenCalled();
});
it('should redirect to create resource policy page', () => {
comp.redirectToResourcePolicyCreatePage();
expect(compAsAny.router.navigate).toHaveBeenCalled();
});
it('should redirect to resource policy edit page', () => {
comp.redirectToResourcePolicyEditPage(resourcePolicy);
expect(compAsAny.router.navigate).toHaveBeenCalled();
});
it('should redirect to resource policy edit page', () => {
compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock)));
comp.redirectToGroupEditPage(resourcePolicy);
expect(compAsAny.router.navigate).toHaveBeenCalled();
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
resourceUUID = 'itemUUID';
resourceType = 'item';
}

View File

@@ -0,0 +1,345 @@
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, from as observableFrom, Observable, Subscription } from 'rxjs';
import { concatMap, distinctUntilChanged, filter, map, reduce, scan, startWith, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service';
import {
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteDataWithNotEmptyPayload,
getSucceededRemoteData
} from '../../core/shared/operators';
import { ResourcePolicy } from '../../core/resource-policy/models/resource-policy.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { Group } from '../../core/eperson/models/group.model';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { RequestService } from '../../core/data/request.service';
import { NotificationsService } from '../notifications/notifications.service';
import { dateToString, stringToNgbDateStruct } from '../date.util';
import { followLink } from '../utils/follow-link-config.model';
import { ADMIN_MODULE_PATH } from '../../app-routing.module';
import { ACCESS_CONTROL_MODULE_PATH } from '../../+admin/admin-routing.module';
import { GROUP_EDIT_PATH } from '../../+admin/admin-access-control/admin-access-control-routing.module';
interface ResourcePolicyCheckboxEntry {
id: string;
policy: ResourcePolicy;
checked: boolean
}
@Component({
selector: 'ds-resource-policies',
styleUrls: ['./resource-policies.component.scss'],
templateUrl: './resource-policies.component.html'
})
/**
* Component that shows the policies for given resource
*/
export class ResourcePoliciesComponent implements OnInit, OnDestroy {
/**
* The resource UUID
* @type {string}
*/
@Input() public resourceUUID: string;
/**
* The resource type (e.g. 'item', 'bundle' etc) used as key to build automatically translation label
* @type {string}
*/
@Input() public resourceType: string;
/**
* A boolean representing if component is active
* @type {boolean}
*/
private isActive: boolean;
/**
* A boolean representing if a submission delete operation is pending
* @type {BehaviorSubject<boolean>}
*/
private processingDelete$ = new BehaviorSubject<boolean>(false);
/**
* The list of policies for given resource
* @type {BehaviorSubject<ResourcePolicyCheckboxEntry[]>}
*/
private resourcePoliciesEntries$: BehaviorSubject<ResourcePolicyCheckboxEntry[]> =
new BehaviorSubject<ResourcePolicyCheckboxEntry[]>([]);
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} cdr
* @param {DSONameService} dsoNameService
* @param {EPersonDataService} ePersonService
* @param {GroupDataService} groupService
* @param {NotificationsService} notificationsService
* @param {RequestService} requestService
* @param {ResourcePolicyService} resourcePolicyService
* @param {ActivatedRoute} route
* @param {Router} router
* @param {TranslateService} translate
*/
constructor(
private cdr: ChangeDetectorRef,
private dsoNameService: DSONameService,
private ePersonService: EPersonDataService,
private groupService: GroupDataService,
private notificationsService: NotificationsService,
private requestService: RequestService,
private resourcePolicyService: ResourcePolicyService,
private route: ActivatedRoute,
private router: Router,
private translate: TranslateService
) {
}
/**
* Initialize the component, setting up the resource's policies
*/
ngOnInit(): void {
this.isActive = true;
this.initResourcePolicyLIst();
}
/**
* Check if there are any selected resource's policies to be deleted
*
* @return {Observable<boolean>}
*/
canDelete(): Observable<boolean> {
return observableFrom(this.resourcePoliciesEntries$.value).pipe(
filter((entry: ResourcePolicyCheckboxEntry) => entry.checked),
reduce((acc: any, value: any) => [...acc, ...value], []),
map((entries: ResourcePolicyCheckboxEntry[]) => isNotEmpty(entries)),
distinctUntilChanged()
)
}
/**
* Delete the selected resource's policies
*/
deleteSelectedResourcePolicies(): void {
this.processingDelete$.next(true);
const policiesToDelete: ResourcePolicyCheckboxEntry[] = this.resourcePoliciesEntries$.value
.filter((entry: ResourcePolicyCheckboxEntry) => entry.checked);
this.subs.push(
observableFrom(policiesToDelete).pipe(
concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)),
scan((acc: any, value: any) => [...acc, ...value], []),
filter((results: boolean[]) => results.length === policiesToDelete.length),
take(1),
).subscribe((results: boolean[]) => {
const failureResults = results.filter((result: boolean) => !result);
if (isEmpty(failureResults)) {
this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content'));
} else {
this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content'));
}
this.initResourcePolicyLIst();
this.processingDelete$.next(false);
})
)
}
/**
* Returns a date in simplified format (YYYY-MM-DD).
*
* @param date
* @return a string with formatted date
*/
formatDate(date: string): string {
return isNotEmpty(date) ? dateToString(stringToNgbDateStruct(date)) : '';
}
/**
* Return the ePerson's name which the given policy is linked to
*
* @param policy The resource policy
*/
getEPersonName(policy: ResourcePolicy): Observable<string> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
// return this.ePersonService.findByHref(policy._links.eperson.href).pipe(
return policy.eperson.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((eperson: EPerson) => this.dsoNameService.getName(eperson)),
startWith('')
)
}
/**
* Return the group's name which the given policy is linked to
*
* @param policy The resource policy
*/
getGroupName(policy: ResourcePolicy): Observable<string> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
// return this.groupService.findByHref(policy._links.group.href).pipe(
return policy.group.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((group: Group) => this.dsoNameService.getName(group)),
startWith('')
)
}
/**
* Return all resource's policies
*
* @return an observable that emits all resource's policies
*/
getResourcePolicies(): Observable<ResourcePolicyCheckboxEntry[]> {
return this.resourcePoliciesEntries$.asObservable();
}
/**
* Check whether the given policy is linked to a ePerson
*
* @param policy The resource policy
* @return an observable that emits true when the policy is linked to a ePerson, false otherwise
*/
hasEPerson(policy): Observable<boolean> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
// return this.ePersonService.findByHref(policy._links.eperson.href).pipe(
return policy.eperson.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((eperson: EPerson) => isNotEmpty(eperson)),
startWith(false)
)
}
/**
* Check whether the given policy is linked to a group
*
* @param policy The resource policy
* @return an observable that emits true when the policy is linked to a group, false otherwise
*/
hasGroup(policy): Observable<boolean> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
// return this.groupService.findByHref(policy._links.group.href).pipe(
return policy.group.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((group: Group) => isNotEmpty(group)),
startWith(false)
)
}
/**
* Initialize the resource's policies list
*/
initResourcePolicyLIst() {
this.resourcePolicyService.searchByResource(this.resourceUUID, null,
followLink('eperson'), followLink('group')).pipe(
filter(() => this.isActive),
getSucceededRemoteData(),
take(1)
).subscribe((result) => {
const entries = result.payload.page.map((policy: ResourcePolicy) => ({
id: policy.id,
policy: policy,
checked: false
}));
this.resourcePoliciesEntries$.next(entries);
// Remove cached request
this.requestService.removeByHrefSubstring(this.resourceUUID);
this.cdr.detectChanges();
});
}
/**
* Return a boolean representing if a delete operation is pending
*
* @return {Observable<boolean>}
*/
isProcessingDelete(): Observable<boolean> {
return this.processingDelete$.asObservable();
}
/**
* Redirect to resource policy creation page
*/
redirectToResourcePolicyCreatePage(): void {
this.router.navigate([`../create`], {
relativeTo: this.route,
queryParams: {
policyTargetId: this.resourceUUID,
targetType: this.resourceType
}
})
}
/**
* Redirect to resource policy editing page
*
* @param policy The resource policy
*/
redirectToResourcePolicyEditPage(policy: ResourcePolicy): void {
this.router.navigate([`../edit`], {
relativeTo: this.route,
queryParams: {
policyId: policy.id
}
})
}
/**
* Redirect to group edit page
*
* @param policy The resource policy
*/
redirectToGroupEditPage(policy: ResourcePolicy): void {
this.requestService.removeByHrefSubstring(policy._links.group.href);
this.subs.push(
this.groupService.findByHref(policy._links.group.href).pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((group: Group) => group.id)
).subscribe((groupUUID) => {
this.router.navigate([ADMIN_MODULE_PATH, ACCESS_CONTROL_MODULE_PATH, GROUP_EDIT_PATH, groupUUID])
})
)
}
/**
* Select/unselect all checkbox in the list
*/
selectAllCheckbox(event: any): void {
const checked = event.target.checked;
this.resourcePoliciesEntries$.value.forEach((entry: ResourcePolicyCheckboxEntry) => entry.checked = checked);
}
/**
* Select/unselect checkbox
*/
selectCheckbox(policyEntry: ResourcePolicyCheckboxEntry, checked: boolean) {
policyEntry.checked = checked;
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.isActive = false;
this.resourcePoliciesEntries$ = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())
}
}

View File

@@ -194,6 +194,14 @@ import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-ta
import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive';
import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component';
import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component'; import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component';
import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component';
import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive';
import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form.component';
import { EpersonGroupListComponent } from './resource-policies/form/eperson-group-list/eperson-group-list.component';
import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/resource-policy-target.resolver';
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -373,7 +381,12 @@ const COMPONENTS = [
PublicationSearchResultListElementComponent, PublicationSearchResultListElementComponent,
ItemVersionsNoticeComponent, ItemVersionsNoticeComponent,
ModifyItemOverviewComponent, ModifyItemOverviewComponent,
ImpersonateNavbarComponent ImpersonateNavbarComponent,
ResourcePoliciesComponent,
ResourcePolicyFormComponent,
EpersonGroupListComponent,
EpersonSearchBoxComponent,
GroupSearchBoxComponent
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -461,7 +474,9 @@ const PROVIDERS = [
{ {
provide: DYNAMIC_FORM_CONTROL_MAP_FN, provide: DYNAMIC_FORM_CONTROL_MAP_FN,
useValue: dsDynamicFormControlMapFn useValue: dsDynamicFormControlMapFn
} },
ResourcePolicyResolver,
ResourcePolicyTargetResolver
]; ];
const DIRECTIVES = [ const DIRECTIVES = [
@@ -475,7 +490,8 @@ const DIRECTIVES = [
RoleDirective, RoleDirective,
MetadataRepresentationDirective, MetadataRepresentationDirective,
ListableObjectDirective, ListableObjectDirective,
ClaimedTaskActionsDirective ClaimedTaskActionsDirective,
NgForTrackByIdDirective
]; ];
@NgModule({ @NgModule({

View File

@@ -14,6 +14,7 @@ export const GroupMock2: Group = Object.assign(new Group(), {
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' }, subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' },
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' } epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' }
}, },
_name: 'testgroupname2',
id: 'testgroupid2', id: 'testgroupid2',
uuid: 'testgroupid2', uuid: 'testgroupid2',
type: 'group', type: 'group',
@@ -32,6 +33,7 @@ export const GroupMock: Group = Object.assign(new Group(), {
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' }, subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' },
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' } epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' }
}, },
_name: 'testgroupname',
id: 'testgroupid', id: 'testgroupid',
uuid: 'testgroupid', uuid: 'testgroupid',
type: 'group', type: 'group',

View File

@@ -3,7 +3,7 @@ import { Component, Input, OnInit } from '@angular/core';
import { find } from 'rxjs/operators'; import { find } from 'rxjs/operators';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { ResourcePolicy } from '../../../../core/shared/resource-policy.model'; import { ResourcePolicy } from '../../../../core/resource-policy/models/resource-policy.model';
import { isEmpty } from '../../../../shared/empty.util'; import { isEmpty } from '../../../../shared/empty.util';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
@@ -42,7 +42,7 @@ export class SubmissionSectionUploadAccessConditionsComponent implements OnInit
ngOnInit() { ngOnInit() {
this.accessConditions.forEach((accessCondition: ResourcePolicy) => { this.accessConditions.forEach((accessCondition: ResourcePolicy) => {
if (isEmpty(accessCondition.name)) { if (isEmpty(accessCondition.name)) {
this.groupService.findById(accessCondition.groupUUID).pipe( this.groupService.findByHref(accessCondition._links.group.href).pipe(
find((rd: RemoteData<Group>) => !rd.isResponsePending && rd.hasSucceeded)) find((rd: RemoteData<Group>) => !rd.isResponsePending && rd.hasSucceeded))
.subscribe((rd: RemoteData<Group>) => { .subscribe((rd: RemoteData<Group>) => {
const group: Group = rd.payload; const group: Group = rd.payload;

View File

@@ -1,7 +1,10 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core'; import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
@@ -20,19 +23,17 @@ import {
mockSubmissionId, mockSubmissionId,
mockSubmissionState, mockSubmissionState,
mockUploadConfigResponse, mockUploadConfigResponse,
mockUploadConfigResponseNotRequired, mockUploadFiles, mockUploadConfigResponseNotRequired,
mockUploadFiles,
} from '../../../shared/mocks/submission.mock'; } from '../../../shared/mocks/submission.mock';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service';
import { SectionUploadService } from './section-upload.service'; import { SectionUploadService } from './section-upload.service';
import { SubmissionSectionUploadComponent } from './section-upload.component'; import { SubmissionSectionUploadComponent } from './section-upload.component';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { cold, hot } from 'jasmine-marbles';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { ResourcePolicyService } from '../../../core/data/resource-policy.service'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { ConfigData } from '../../../core/config/config-data'; import { ConfigData } from '../../../core/config/config-data';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';

View File

@@ -8,7 +8,7 @@ import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shar
import { SectionUploadService } from './section-upload.service'; import { SectionUploadService } from './section-upload.service';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { ResourcePolicyService } from '../../../core/data/resource-policy.service'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service';
import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model';
import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model';

View File

@@ -996,6 +996,11 @@
"item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.",
"item.edit.authorizations.title": "Edit item's Policies",
"item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle": "Bundle",
@@ -2066,6 +2071,100 @@
"resource-policies.add.button": "Add",
"resource-policies.add.for.": "Add a new policy",
"resource-policies.add.for.bitstream": "Add a new Bitstream policy",
"resource-policies.add.for.bundle": "Add a new Bundle policy",
"resource-policies.add.for.item": "Add a new Item policy",
"resource-policies.create.page.heading": "Create new resource policy for ",
"resource-policies.create.page.failure.content": "An error occurred while creating the resource policy.",
"resource-policies.create.page.success.content": "Operation successful",
"resource-policies.create.page.title": "Create new resource policy",
"resource-policies.delete.btn": "Delete selected",
"resource-policies.delete.btn.title": "Delete selected resource policies",
"resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.",
"resource-policies.delete.success.content": "Operation successful",
"resource-policies.edit.page.heading": "Edit resource policy ",
"resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.",
"resource-policies.edit.page.success.content": "Operation successful",
"resource-policies.edit.page.title": "Edit resource policy",
"resource-policies.form.action-type.label": "Select the action type",
"resource-policies.form.action-type.required": "You must select the resource policy action.",
"resource-policies.form.eperson-group-list.label": "The eperson or group that will be grant of the permission",
"resource-policies.form.eperson-group-list.select.btn": "Select",
"resource-policies.form.eperson-group-list.tab.eperson": "Search for a ePerson",
"resource-policies.form.eperson-group-list.tab.group": "Search for a group",
"resource-policies.form.eperson-group-list.table.headers.action": "Action",
"resource-policies.form.eperson-group-list.table.headers.id": "ID",
"resource-policies.form.eperson-group-list.table.headers.name": "Name",
"resource-policies.form.date.end.label": "End Date",
"resource-policies.form.date.start.label": "Start Date",
"resource-policies.form.description.label": "Description",
"resource-policies.form.name.label": "Name",
"resource-policies.form.policy-type.label": "Select the policy type",
"resource-policies.form.policy-type.required": "You must select the resource policy type.",
"resource-policies.table.headers.action": "Action",
"resource-policies.table.headers.date.end": "End Date",
"resource-policies.table.headers.date.start": "Start Date",
"resource-policies.table.headers.edit": "Edit",
"resource-policies.table.headers.edit.group": "Edit group",
"resource-policies.table.headers.edit.policy": "Edit policy",
"resource-policies.table.headers.eperson": "EPerson",
"resource-policies.table.headers.group": "Group",
"resource-policies.table.headers.id": "ID",
"resource-policies.table.headers.name": "Name",
"resource-policies.table.headers.policyType": "type",
"resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream",
"resource-policies.table.headers.title.for.bundle": "Policies for Bundle",
"resource-policies.table.headers.title.for.item": "Policies for Item",
"search.description": "", "search.description": "",
"search.switch-configuration.title": "Show", "search.switch-configuration.title": "Show",