mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #1327 from atmire/w2p-83635_Request-a-copy
Request a copy
This commit is contained in:
@@ -4,7 +4,7 @@ import { Collection } from './core/shared/collection.model';
|
||||
import { Item } from './core/shared/item.model';
|
||||
import { getCommunityPageRoute } from './community-page/community-page-routing-paths';
|
||||
import { getCollectionPageRoute } from './collection-page/collection-page-routing-paths';
|
||||
import { getItemPageRoute } from './item-page/item-page-routing-paths';
|
||||
import { getItemModuleRoute, getItemPageRoute } from './item-page/item-page-routing-paths';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { URLCombiner } from './core/url-combiner/url-combiner';
|
||||
|
||||
@@ -22,6 +22,15 @@ export function getBitstreamModuleRoute() {
|
||||
export function getBitstreamDownloadRoute(bitstream): string {
|
||||
return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString();
|
||||
}
|
||||
export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: string, queryParams: any } {
|
||||
const url = new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString();
|
||||
return {
|
||||
routerLink: url,
|
||||
queryParams: {
|
||||
bitstream: bitstream.uuid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const ADMIN_MODULE_PATH = 'admin';
|
||||
|
||||
@@ -90,3 +99,8 @@ export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
export function getAccessControlModuleRoute() {
|
||||
return `/${ACCESS_CONTROL_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export const REQUEST_COPY_MODULE_PATH = 'request-a-copy';
|
||||
export function getRequestCopyModulePath() {
|
||||
return `/${REQUEST_COPY_MODULE_PATH}`;
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
PROFILE_MODULE_PATH,
|
||||
REGISTER_PATH,
|
||||
WORKFLOW_ITEM_MODULE_PATH,
|
||||
LEGACY_BITSTREAM_MODULE_PATH,
|
||||
LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH,
|
||||
} from './app-routing-paths';
|
||||
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
||||
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
||||
@@ -180,6 +180,11 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
path: INFO_MODULE_PATH,
|
||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
||||
},
|
||||
{
|
||||
path: REQUEST_COPY_MODULE_PATH,
|
||||
loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{
|
||||
path: FORBIDDEN_PATH,
|
||||
component: ThemedForbiddenComponent
|
||||
|
@@ -14,6 +14,7 @@ export enum FeatureID {
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
CanDownload = 'canDownload',
|
||||
CanRequestACopy = 'canRequestACopy',
|
||||
CanManageVersions = 'canManageVersions',
|
||||
CanManageBitstreamBundles = 'canManageBitstreamBundles',
|
||||
CanManageRelationships = 'canManageRelationships',
|
||||
|
95
src/app/core/data/item-request-data.service.spec.ts
Normal file
95
src/app/core/data/item-request-data.service.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ItemRequestDataService } from './item-request-data.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { ItemRequest } from '../shared/item-request.model';
|
||||
import { PostRequest } from './request.models';
|
||||
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
|
||||
describe('ItemRequestDataService', () => {
|
||||
let service: ItemRequestDataService;
|
||||
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let halService: HALEndpointService;
|
||||
|
||||
const restApiEndpoint = 'rest/api/endpoint/';
|
||||
const requestId = 'request-id';
|
||||
let itemRequest: ItemRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
itemRequest = Object.assign(new ItemRequest(), {
|
||||
token: 'item-request-token',
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestId,
|
||||
send: '',
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildFromRequestUUID: createSuccessfulRemoteDataObject$(itemRequest),
|
||||
});
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: observableOf(restApiEndpoint),
|
||||
});
|
||||
|
||||
service = new ItemRequestDataService(requestService, rdbService, null, null, halService, null, null, null);
|
||||
});
|
||||
|
||||
describe('requestACopy', () => {
|
||||
it('should send a POST request containing the provided item request', (done) => {
|
||||
service.requestACopy(itemRequest).subscribe(() => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestId, restApiEndpoint, itemRequest));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('grant', () => {
|
||||
let email: RequestCopyEmail;
|
||||
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
});
|
||||
|
||||
it('should send a PUT request containing the correct properties', (done) => {
|
||||
service.grant(itemRequest.token, email, true).subscribe(() => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
method: RestRequestMethod.PUT,
|
||||
body: JSON.stringify({
|
||||
acceptRequest: true,
|
||||
responseMessage: email.message,
|
||||
subject: email.subject,
|
||||
suggestOpenAccess: true,
|
||||
}),
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deny', () => {
|
||||
let email: RequestCopyEmail;
|
||||
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
});
|
||||
|
||||
it('should send a PUT request containing the correct properties', (done) => {
|
||||
service.deny(itemRequest.token, email).subscribe(() => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
method: RestRequestMethod.PUT,
|
||||
body: JSON.stringify({
|
||||
acceptRequest: false,
|
||||
responseMessage: email.message,
|
||||
subject: email.subject,
|
||||
suggestOpenAccess: false,
|
||||
}),
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
131
src/app/core/data/item-request-data.service.ts
Normal file
131
src/app/core/data/item-request-data.service.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, find, map } from 'rxjs/operators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { getFirstCompletedRemoteData, sendRequest } from '../shared/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PostRequest, PutRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { ItemRequest } from '../shared/item-request.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { DataService } from './data.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint
|
||||
*/
|
||||
@Injectable(
|
||||
{
|
||||
providedIn: 'root',
|
||||
}
|
||||
)
|
||||
export class ItemRequestDataService extends DataService<ItemRequest> {
|
||||
|
||||
protected linkPath = 'itemrequests';
|
||||
|
||||
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: DefaultChangeAnalyzer<ItemRequest>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getItemRequestEndpoint(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for an {@link ItemRequest} by their token
|
||||
* @param token
|
||||
*/
|
||||
getItemRequestEndpointByToken(token: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => `${href}/${token}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a copy of an item
|
||||
* @param itemRequest
|
||||
*/
|
||||
requestACopy(itemRequest: ItemRequest): Observable<RemoteData<ItemRequest>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const href$ = this.getItemRequestEndpoint();
|
||||
|
||||
href$.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PostRequest(requestId, href, itemRequest);
|
||||
this.requestService.send(request);
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
return this.rdbService.buildFromRequestUUID<ItemRequest>(requestId).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the request of an item
|
||||
* @param token Token of the {@link ItemRequest}
|
||||
* @param email Email to send back to the user requesting the item
|
||||
*/
|
||||
deny(token: string, email: RequestCopyEmail): Observable<RemoteData<ItemRequest>> {
|
||||
return this.process(token, email, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant the request of an item
|
||||
* @param token Token of the {@link ItemRequest}
|
||||
* @param email Email to send back to the user requesting the item
|
||||
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||
*/
|
||||
grant(token: string, email: RequestCopyEmail, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
||||
return this.process(token, email, true, suggestOpenAccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the request of an item
|
||||
* @param token Token of the {@link ItemRequest}
|
||||
* @param email Email to send back to the user requesting the item
|
||||
* @param grant Grant or deny the request (true = grant, false = deny)
|
||||
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||
*/
|
||||
process(token: string, email: RequestCopyEmail, grant: boolean, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
this.getItemRequestEndpointByToken(token).pipe(
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/json');
|
||||
options.headers = headers;
|
||||
return new PutRequest(requestId, endpointURL, JSON.stringify({
|
||||
acceptRequest: grant,
|
||||
responseMessage: email.message,
|
||||
subject: email.subject,
|
||||
suggestOpenAccess,
|
||||
}), options);
|
||||
}),
|
||||
sendRequest(this.requestService)).subscribe();
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId);
|
||||
}
|
||||
|
||||
}
|
90
src/app/core/shared/item-request.model.ts
Normal file
90
src/app/core/shared/item-request.model.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../cache/builders/build-decorators';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { ITEM_REQUEST } from './item-request.resource-type';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { HALLink } from './hal-link.model';
|
||||
|
||||
/**
|
||||
* Model class for an ItemRequest
|
||||
*/
|
||||
@typedObject
|
||||
export class ItemRequest implements CacheableObject {
|
||||
static type = ITEM_REQUEST;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* opaque string which uniquely identifies this request
|
||||
*/
|
||||
@autoserialize
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* true if the request is for all bitstreams of the item.
|
||||
*/
|
||||
@autoserialize
|
||||
allfiles: boolean;
|
||||
/**
|
||||
* email address of the person requesting the files.
|
||||
*/
|
||||
@autoserialize
|
||||
requestEmail: string;
|
||||
/**
|
||||
* Human-readable name of the person requesting the files.
|
||||
*/
|
||||
@autoserialize
|
||||
requestName: string;
|
||||
/**
|
||||
* arbitrary message provided by the person requesting the files.
|
||||
*/
|
||||
@autoserialize
|
||||
requestMessage: string;
|
||||
/**
|
||||
* date that the request was recorded.
|
||||
*/
|
||||
@autoserialize
|
||||
requestDate: string;
|
||||
/**
|
||||
* true if the request has been granted.
|
||||
*/
|
||||
@autoserialize
|
||||
acceptRequest: boolean;
|
||||
/**
|
||||
* date that the request was granted or denied.
|
||||
*/
|
||||
@autoserialize
|
||||
decisionDate: string;
|
||||
/**
|
||||
* date on which the request is considered expired.
|
||||
*/
|
||||
@autoserialize
|
||||
expires: string;
|
||||
/**
|
||||
* UUID of the requested Item.
|
||||
*/
|
||||
@autoserialize
|
||||
itemId: string;
|
||||
/**
|
||||
* UUID of the requested bitstream.
|
||||
*/
|
||||
@autoserialize
|
||||
bitstreamId: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this ItemRequest
|
||||
*/
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink;
|
||||
item: HALLink;
|
||||
bitstream: HALLink;
|
||||
};
|
||||
|
||||
}
|
9
src/app/core/shared/item-request.resource-type.ts
Normal file
9
src/app/core/shared/item-request.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for ItemRequest.
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const ITEM_REQUEST = new ResourceType('itemrequest');
|
@@ -33,7 +33,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ds-file-download-link [bitstream]="file">
|
||||
<ds-file-download-link [bitstream]="file" [item]="item">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ds-file-download-link [bitstream]="file">
|
||||
<ds-file-download-link [bitstream]="file" [item]="item">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
|
@@ -12,6 +12,7 @@ import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -42,6 +43,10 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon
|
||||
path: UPLOAD_BITSTREAM_PATH,
|
||||
component: UploadBitstreamComponent,
|
||||
canActivate: [AuthenticatedGuard]
|
||||
},
|
||||
{
|
||||
path: ':request-a-copy',
|
||||
component: BitstreamRequestACopyPageComponent,
|
||||
}
|
||||
],
|
||||
data: {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||
<div class="file-section">
|
||||
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file">
|
||||
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file" [item]="item">
|
||||
<span>{{file?.name}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||
|
@@ -0,0 +1,9 @@
|
||||
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||
<h3 class="mb-4">{{'deny-request-copy.header' | translate}}</h3>
|
||||
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
|
||||
<p>{{'deny-request-copy.intro' | translate}}</p>
|
||||
|
||||
<ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="deny($event)"></ds-email-request-copy>
|
||||
</div>
|
||||
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading>
|
||||
</div>
|
@@ -0,0 +1,177 @@
|
||||
import { DenyRequestCopyComponent } from './deny-request-copy.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||
|
||||
describe('DenyRequestCopyComponent', () => {
|
||||
let component: DenyRequestCopyComponent;
|
||||
let fixture: ComponentFixture<DenyRequestCopyComponent>;
|
||||
|
||||
let router: Router;
|
||||
let route: ActivatedRoute;
|
||||
let authService: AuthService;
|
||||
let translateService: TranslateService;
|
||||
let itemDataService: ItemDataService;
|
||||
let nameService: DSONameService;
|
||||
let itemRequestService: ItemRequestDataService;
|
||||
let notificationsService: NotificationsService;
|
||||
|
||||
let itemRequest: ItemRequest;
|
||||
let user: EPerson;
|
||||
let item: Item;
|
||||
let itemName: string;
|
||||
let itemUrl: string;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
itemRequest = Object.assign(new ItemRequest(), {
|
||||
token: 'item-request-token',
|
||||
requestName: 'requester name'
|
||||
});
|
||||
user = Object.assign(new EPerson(), {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: 'first'
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: 'last'
|
||||
}
|
||||
]
|
||||
},
|
||||
email: 'user-email',
|
||||
});
|
||||
itemName = 'item-name';
|
||||
itemUrl = 'item-url';
|
||||
item = Object.assign(new Item(), {
|
||||
id: 'item-id',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
value: itemUrl
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: itemName
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||
});
|
||||
route = jasmine.createSpyObj('route', {}, {
|
||||
data: observableOf({
|
||||
request: createSuccessfulRemoteDataObject(itemRequest),
|
||||
}),
|
||||
});
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
getAuthenticatedUserFromStore: observableOf(user),
|
||||
});
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(item),
|
||||
});
|
||||
nameService = jasmine.createSpyObj('nameService', {
|
||||
getName: itemName,
|
||||
});
|
||||
itemRequestService = jasmine.createSpyObj('itemRequestService', {
|
||||
deny: createSuccessfulRemoteDataObject$(itemRequest),
|
||||
});
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DenyRequestCopyComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DenyRequestCopyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
translateService = (component as any).translateService;
|
||||
spyOn(translateService, 'get').and.returnValue(observableOf('translated-message'));
|
||||
});
|
||||
|
||||
it('message$ should be parameterized correctly', (done) => {
|
||||
component.message$.subscribe(() => {
|
||||
expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({
|
||||
recipientName: itemRequest.requestName,
|
||||
itemUrl: itemUrl,
|
||||
itemName: itemName,
|
||||
authorName: user.name,
|
||||
authorEmail: user.email,
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deny', () => {
|
||||
let email: RequestCopyEmail;
|
||||
|
||||
describe('when the request is successful', () => {
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
(itemRequestService.deny as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(itemRequest));
|
||||
component.deny(email);
|
||||
});
|
||||
|
||||
it('should display a success notification', () => {
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to the homepage', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the request is unsuccessful', () => {
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
(itemRequestService.deny as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
component.deny(email);
|
||||
});
|
||||
|
||||
it('should display a success notification', () => {
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not navigate', () => {
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,112 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import {
|
||||
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
|
||||
redirectOn4xx
|
||||
} from '../../core/shared/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-deny-request-copy',
|
||||
styleUrls: ['./deny-request-copy.component.scss'],
|
||||
templateUrl: './deny-request-copy.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for denying an item request
|
||||
*/
|
||||
export class DenyRequestCopyComponent implements OnInit {
|
||||
/**
|
||||
* The item request to deny
|
||||
*/
|
||||
itemRequestRD$: Observable<RemoteData<ItemRequest>>;
|
||||
|
||||
/**
|
||||
* The default subject of the message to send to the user requesting the item
|
||||
*/
|
||||
subject$: Observable<string>;
|
||||
/**
|
||||
* The default contents of the message to send to the user requesting the item
|
||||
*/
|
||||
message$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private authService: AuthService,
|
||||
private translateService: TranslateService,
|
||||
private itemDataService: ItemDataService,
|
||||
private nameService: DSONameService,
|
||||
private itemRequestService: ItemRequestDataService,
|
||||
private notificationsService: NotificationsService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemRequestRD$ = this.route.data.pipe(
|
||||
map((data) => data.request as RemoteData<ItemRequest>),
|
||||
getFirstCompletedRemoteData(),
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
);
|
||||
|
||||
const msgParams$ = observableCombineLatest(
|
||||
this.itemRequestRD$.pipe(getFirstSucceededRemoteDataPayload()),
|
||||
this.authService.getAuthenticatedUserFromStore(),
|
||||
).pipe(
|
||||
switchMap(([itemRequest, user]: [ItemRequest, EPerson]) => {
|
||||
return this.itemDataService.findById(itemRequest.itemId).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((item: Item) => {
|
||||
const uri = item.firstMetadataValue('dc.identifier.uri');
|
||||
return Object.assign({
|
||||
recipientName: itemRequest.requestName,
|
||||
itemUrl: isNotEmpty(uri) ? uri : item.handle,
|
||||
itemName: this.nameService.getName(item),
|
||||
authorName: user.name,
|
||||
authorEmail: user.email,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
this.subject$ = this.translateService.get('deny-request-copy.email.subject');
|
||||
this.message$ = msgParams$.pipe(
|
||||
switchMap((params) => this.translateService.get('deny-request-copy.email.message', params)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the item request
|
||||
* @param email Subject and contents of the message to send back to the user requesting the item
|
||||
*/
|
||||
deny(email: RequestCopyEmail) {
|
||||
this.itemRequestRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((itemRequest: ItemRequest) => this.itemRequestService.deny(itemRequest.token, email)),
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('deny-request-copy.success'));
|
||||
this.router.navigateByUrl('/');
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('deny-request-copy.error'), rd.errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="subject">{{ 'grant-deny-request-copy.email.subject' | translate }}</label>
|
||||
<input type="text" class="form-control" id="subject" [ngClass]="{'is-invalid': !subject || subject.length === 0}" [(ngModel)]="subject" name="subject">
|
||||
<div *ngIf="!subject || subject.length === 0" class="invalid-feedback">
|
||||
{{ 'grant-deny-request-copy.email.subject.empty' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">{{ 'grant-deny-request-copy.email.message' | translate }}</label>
|
||||
<textarea class="form-control" id="message" rows="8" [ngClass]="{'is-invalid': !message || message.length === 0}" [(ngModel)]="message" name="message"></textarea>
|
||||
<div *ngIf="!message || message.length === 0" class="invalid-feedback">
|
||||
{{ 'grant-deny-request-copy.email.message.empty' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<button (click)="submit()"
|
||||
[disabled]="!message || message.length === 0 || !subject || subject.length === 0"
|
||||
class="btn btn-primary"
|
||||
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
||||
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
|
||||
</button>
|
||||
<button (click)="return()"
|
||||
class="btn btn-outline-secondary mr-1"
|
||||
title="{{'grant-deny-request-copy.email.back' | translate }}">
|
||||
<i class="fas fa-arrow-left"></i> {{'grant-deny-request-copy.email.back' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,47 @@
|
||||
import { EmailRequestCopyComponent } from './email-request-copy.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { RequestCopyEmail } from './request-copy-email.model';
|
||||
|
||||
describe('EmailRequestCopyComponent', () => {
|
||||
let component: EmailRequestCopyComponent;
|
||||
let fixture: ComponentFixture<EmailRequestCopyComponent>;
|
||||
|
||||
let location: Location;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
location = jasmine.createSpyObj('location', ['back']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [EmailRequestCopyComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: Location, useValue: location },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EmailRequestCopyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('return should navigate to the previous page', () => {
|
||||
component.return();
|
||||
expect(location.back).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submit should emit an email object', () => {
|
||||
spyOn(component.send, 'emit').and.stub();
|
||||
component.subject = 'test-subject';
|
||||
component.message = 'test-message';
|
||||
component.submit();
|
||||
expect(component.send.emit).toHaveBeenCalledWith(new RequestCopyEmail('test-subject', 'test-message'));
|
||||
});
|
||||
});
|
@@ -0,0 +1,45 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { RequestCopyEmail } from './request-copy-email.model';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-email-request-copy',
|
||||
styleUrls: ['./email-request-copy.component.scss'],
|
||||
templateUrl: './email-request-copy.component.html'
|
||||
})
|
||||
/**
|
||||
* A form component for an email to send back to the user requesting an item
|
||||
*/
|
||||
export class EmailRequestCopyComponent {
|
||||
/**
|
||||
* Event emitter for sending the email
|
||||
*/
|
||||
@Output() send: EventEmitter<RequestCopyEmail> = new EventEmitter<RequestCopyEmail>();
|
||||
|
||||
/**
|
||||
* The subject of the email
|
||||
*/
|
||||
@Input() subject: string;
|
||||
|
||||
/**
|
||||
* The contents of the email
|
||||
*/
|
||||
@Input() message: string;
|
||||
|
||||
constructor(protected location: Location) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the email
|
||||
*/
|
||||
submit() {
|
||||
this.send.emit(new RequestCopyEmail(this.subject, this.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return to the previous page
|
||||
*/
|
||||
return() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* A class representing an email to send back to the user requesting an item
|
||||
*/
|
||||
export class RequestCopyEmail {
|
||||
constructor(public subject: string,
|
||||
public message: string) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||
<h3 class="mb-4">{{'grant-deny-request-copy.header' | translate}}</h3>
|
||||
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
|
||||
<div *ngIf="!itemRequestRD.payload.decisionDate">
|
||||
<p [innerHTML]="'grant-deny-request-copy.intro1' | translate:{ url: (itemUrl$ | async), name: (itemName$ | async) }"></p>
|
||||
<p>{{'grant-deny-request-copy.intro2' | translate}}</p>
|
||||
|
||||
<div class="btn-group ">
|
||||
<a [routerLink]="grantRoute$ | async"
|
||||
class="btn btn-outline-primary"
|
||||
title="{{'grant-deny-request-copy.grant' | translate }}">
|
||||
{{'grant-deny-request-copy.grant' | translate }}
|
||||
</a>
|
||||
|
||||
<a [routerLink]="denyRoute$ | async"
|
||||
class="btn btn-outline-danger"
|
||||
title="{{'grant-deny-request-copy.deny' | translate }}">
|
||||
{{'grant-deny-request-copy.deny' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="itemRequestRD.payload.decisionDate" class="processed-message">
|
||||
<p>{{'grant-deny-request-copy.processed' | translate}}</p>
|
||||
<p class="text-center">
|
||||
<a routerLink="/home" class="btn btn-primary">{{'grant-deny-request-copy.home-page' | translate}}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading>
|
||||
</div>
|
@@ -0,0 +1,141 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy.component';
|
||||
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
||||
import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('GrantDenyRequestCopyComponent', () => {
|
||||
let component: GrantDenyRequestCopyComponent;
|
||||
let fixture: ComponentFixture<GrantDenyRequestCopyComponent>;
|
||||
|
||||
let router: Router;
|
||||
let route: ActivatedRoute;
|
||||
let authService: AuthService;
|
||||
let itemDataService: ItemDataService;
|
||||
let nameService: DSONameService;
|
||||
|
||||
let itemRequest: ItemRequest;
|
||||
let item: Item;
|
||||
let itemName: string;
|
||||
let itemUrl: string;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
itemRequest = Object.assign(new ItemRequest(), {
|
||||
token: 'item-request-token',
|
||||
requestName: 'requester name'
|
||||
});
|
||||
itemName = 'item-name';
|
||||
item = Object.assign(new Item(), {
|
||||
id: 'item-id',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
value: itemUrl
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: itemName
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
itemUrl = getItemPageRoute(item);
|
||||
|
||||
route = jasmine.createSpyObj('route', {}, {
|
||||
data: observableOf({
|
||||
request: createSuccessfulRemoteDataObject(itemRequest),
|
||||
}),
|
||||
});
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
});
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(item),
|
||||
});
|
||||
nameService = jasmine.createSpyObj('nameService', {
|
||||
getName: itemName,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [GrantDenyRequestCopyComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GrantDenyRequestCopyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
router = (component as any).router;
|
||||
spyOn(router, 'navigateByUrl').and.stub();
|
||||
});
|
||||
|
||||
it('should initialise itemName$', (done) => {
|
||||
component.itemName$.subscribe((result) => {
|
||||
expect(result).toEqual(itemName);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialise itemUrl$', (done) => {
|
||||
component.itemUrl$.subscribe((result) => {
|
||||
expect(result).toEqual(itemUrl);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialise denyRoute$', (done) => {
|
||||
component.denyRoute$.subscribe((result) => {
|
||||
expect(result).toEqual(getRequestCopyDenyRoute(itemRequest.token));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialise grantRoute$', (done) => {
|
||||
component.grantRoute$.subscribe((result) => {
|
||||
expect(result).toEqual(getRequestCopyGrantRoute(itemRequest.token));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('processed message', () => {
|
||||
it('should not be displayed when decisionDate is undefined', () => {
|
||||
const message = fixture.debugElement.query(By.css('.processed-message'));
|
||||
expect(message).toBeNull();
|
||||
});
|
||||
|
||||
it('should be displayed when decisionDate is defined', () => {
|
||||
component.itemRequestRD$ = createSuccessfulRemoteDataObject$(Object.assign(new ItemRequest(), itemRequest, {
|
||||
decisionDate: 'defined-date'
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
|
||||
const message = fixture.debugElement.query(By.css('.processed-message'));
|
||||
expect(message).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,97 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
redirectOn4xx
|
||||
} from '../../core/shared/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-grant-deny-request-copy',
|
||||
styleUrls: ['./grant-deny-request-copy.component.scss'],
|
||||
templateUrl: './grant-deny-request-copy.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for an author to decide to grant or deny an item request
|
||||
*/
|
||||
export class GrantDenyRequestCopyComponent implements OnInit {
|
||||
/**
|
||||
* The item request to grant or deny
|
||||
*/
|
||||
itemRequestRD$: Observable<RemoteData<ItemRequest>>;
|
||||
|
||||
/**
|
||||
* The item the request is requesting access to
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The name of the item
|
||||
*/
|
||||
itemName$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The url of the item
|
||||
*/
|
||||
itemUrl$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The route to the page for denying access to the item
|
||||
*/
|
||||
denyRoute$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The route to the page for granting access to the item
|
||||
*/
|
||||
grantRoute$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private authService: AuthService,
|
||||
private itemDataService: ItemDataService,
|
||||
private nameService: DSONameService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemRequestRD$ = this.route.data.pipe(
|
||||
map((data) => data.request as RemoteData<ItemRequest>),
|
||||
getFirstCompletedRemoteData(),
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
);
|
||||
this.itemRD$ = this.itemRequestRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((itemRequest: ItemRequest) => this.itemDataService.findById(itemRequest.itemId)),
|
||||
);
|
||||
this.itemName$ = this.itemRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((item) => this.nameService.getName(item)),
|
||||
);
|
||||
this.itemUrl$ = this.itemRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((item) => getItemPageRoute(item)),
|
||||
);
|
||||
|
||||
this.denyRoute$ = this.itemRequestRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((itemRequest: ItemRequest) => getRequestCopyDenyRoute(itemRequest.token))
|
||||
);
|
||||
this.grantRoute$ = this.itemRequestRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((itemRequest: ItemRequest) => getRequestCopyGrantRoute(itemRequest.token))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||
<h3 class="mb-4">{{'grant-request-copy.header' | translate}}</h3>
|
||||
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
|
||||
<p>{{'grant-request-copy.intro' | translate}}</p>
|
||||
|
||||
<ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="grant($event)">
|
||||
<p>{{ 'grant-deny-request-copy.email.permissions.info' | translate }}</p>
|
||||
<form class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="permissions" [(ngModel)]="suggestOpenAccess" name="permissions">
|
||||
<label class="form-check-label" for="permissions">{{ 'grant-deny-request-copy.email.permissions.label' | translate }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</ds-email-request-copy>
|
||||
</div>
|
||||
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading>
|
||||
</div>
|
@@ -0,0 +1,177 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||
import { GrantRequestCopyComponent } from './grant-request-copy.component';
|
||||
|
||||
describe('GrantRequestCopyComponent', () => {
|
||||
let component: GrantRequestCopyComponent;
|
||||
let fixture: ComponentFixture<GrantRequestCopyComponent>;
|
||||
|
||||
let router: Router;
|
||||
let route: ActivatedRoute;
|
||||
let authService: AuthService;
|
||||
let translateService: TranslateService;
|
||||
let itemDataService: ItemDataService;
|
||||
let nameService: DSONameService;
|
||||
let itemRequestService: ItemRequestDataService;
|
||||
let notificationsService: NotificationsService;
|
||||
|
||||
let itemRequest: ItemRequest;
|
||||
let user: EPerson;
|
||||
let item: Item;
|
||||
let itemName: string;
|
||||
let itemUrl: string;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
itemRequest = Object.assign(new ItemRequest(), {
|
||||
token: 'item-request-token',
|
||||
requestName: 'requester name'
|
||||
});
|
||||
user = Object.assign(new EPerson(), {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: 'first'
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: 'last'
|
||||
}
|
||||
]
|
||||
},
|
||||
email: 'user-email',
|
||||
});
|
||||
itemName = 'item-name';
|
||||
itemUrl = 'item-url';
|
||||
item = Object.assign(new Item(), {
|
||||
id: 'item-id',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
value: itemUrl
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: itemName
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||
});
|
||||
route = jasmine.createSpyObj('route', {}, {
|
||||
data: observableOf({
|
||||
request: createSuccessfulRemoteDataObject(itemRequest),
|
||||
}),
|
||||
});
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
getAuthenticatedUserFromStore: observableOf(user),
|
||||
});
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(item),
|
||||
});
|
||||
nameService = jasmine.createSpyObj('nameService', {
|
||||
getName: itemName,
|
||||
});
|
||||
itemRequestService = jasmine.createSpyObj('itemRequestService', {
|
||||
grant: createSuccessfulRemoteDataObject$(itemRequest),
|
||||
});
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [GrantRequestCopyComponent, VarDirective],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GrantRequestCopyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
translateService = (component as any).translateService;
|
||||
spyOn(translateService, 'get').and.returnValue(observableOf('translated-message'));
|
||||
});
|
||||
|
||||
it('message$ should be parameterized correctly', (done) => {
|
||||
component.message$.subscribe(() => {
|
||||
expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({
|
||||
recipientName: itemRequest.requestName,
|
||||
itemUrl: itemUrl,
|
||||
itemName: itemName,
|
||||
authorName: user.name,
|
||||
authorEmail: user.email,
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('grant', () => {
|
||||
let email: RequestCopyEmail;
|
||||
|
||||
describe('when the request is successful', () => {
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
(itemRequestService.grant as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(itemRequest));
|
||||
component.grant(email);
|
||||
});
|
||||
|
||||
it('should display a success notification', () => {
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to the homepage', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the request is unsuccessful', () => {
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
(itemRequestService.grant as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
component.grant(email);
|
||||
});
|
||||
|
||||
it('should display a success notification', () => {
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not navigate', () => {
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,118 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import {
|
||||
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
|
||||
redirectOn4xx
|
||||
} from '../../core/shared/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-grant-request-copy',
|
||||
styleUrls: ['./grant-request-copy.component.scss'],
|
||||
templateUrl: './grant-request-copy.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for granting an item request
|
||||
*/
|
||||
export class GrantRequestCopyComponent implements OnInit {
|
||||
/**
|
||||
* The item request to accept
|
||||
*/
|
||||
itemRequestRD$: Observable<RemoteData<ItemRequest>>;
|
||||
|
||||
/**
|
||||
* The default subject of the message to send to the user requesting the item
|
||||
*/
|
||||
subject$: Observable<string>;
|
||||
/**
|
||||
* The default contents of the message to send to the user requesting the item
|
||||
*/
|
||||
message$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Whether or not the item should be open access, to avoid future requests
|
||||
* Defaults to false
|
||||
*/
|
||||
suggestOpenAccess = false;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private authService: AuthService,
|
||||
private translateService: TranslateService,
|
||||
private itemDataService: ItemDataService,
|
||||
private nameService: DSONameService,
|
||||
private itemRequestService: ItemRequestDataService,
|
||||
private notificationsService: NotificationsService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemRequestRD$ = this.route.data.pipe(
|
||||
map((data) => data.request as RemoteData<ItemRequest>),
|
||||
getFirstCompletedRemoteData(),
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
);
|
||||
|
||||
const msgParams$ = observableCombineLatest(
|
||||
this.itemRequestRD$.pipe(getFirstSucceededRemoteDataPayload()),
|
||||
this.authService.getAuthenticatedUserFromStore(),
|
||||
).pipe(
|
||||
switchMap(([itemRequest, user]: [ItemRequest, EPerson]) => {
|
||||
return this.itemDataService.findById(itemRequest.itemId).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((item: Item) => {
|
||||
const uri = item.firstMetadataValue('dc.identifier.uri');
|
||||
return Object.assign({
|
||||
recipientName: itemRequest.requestName,
|
||||
itemUrl: isNotEmpty(uri) ? uri : item.handle,
|
||||
itemName: this.nameService.getName(item),
|
||||
authorName: user.name,
|
||||
authorEmail: user.email,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
this.subject$ = this.translateService.get('grant-request-copy.email.subject');
|
||||
this.message$ = msgParams$.pipe(
|
||||
switchMap((params) => this.translateService.get('grant-request-copy.email.message', params)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant the item request
|
||||
* @param email Subject and contents of the message to send back to the user requesting the item
|
||||
*/
|
||||
grant(email: RequestCopyEmail) {
|
||||
this.itemRequestRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((itemRequest: ItemRequest) => this.itemRequestService.grant(itemRequest.token, email, this.suggestOpenAccess)),
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('grant-request-copy.success'));
|
||||
this.router.navigateByUrl('/');
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('grant-request-copy.error'), rd.errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
18
src/app/request-copy/request-copy-routing-paths.ts
Normal file
18
src/app/request-copy/request-copy-routing-paths.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getRequestCopyModulePath } from '../app-routing-paths';
|
||||
|
||||
export function getRequestCopyRoute(token: string) {
|
||||
return new URLCombiner(getRequestCopyModulePath(), token).toString();
|
||||
}
|
||||
|
||||
export const REQUEST_COPY_DENY_PATH = 'deny';
|
||||
|
||||
export function getRequestCopyDenyRoute(token: string) {
|
||||
return new URLCombiner(getRequestCopyRoute(token), REQUEST_COPY_DENY_PATH).toString();
|
||||
}
|
||||
|
||||
export const REQUEST_COPY_GRANT_PATH = 'grant';
|
||||
|
||||
export function getRequestCopyGrantRoute(token: string) {
|
||||
return new URLCombiner(getRequestCopyRoute(token), REQUEST_COPY_GRANT_PATH).toString();
|
||||
}
|
40
src/app/request-copy/request-copy-routing.module.ts
Normal file
40
src/app/request-copy/request-copy-routing.module.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RequestCopyResolver } from './request-copy.resolver';
|
||||
import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component';
|
||||
import { REQUEST_COPY_DENY_PATH, REQUEST_COPY_GRANT_PATH } from './request-copy-routing-paths';
|
||||
import { DenyRequestCopyComponent } from './deny-request-copy/deny-request-copy.component';
|
||||
import { GrantRequestCopyComponent } from './grant-request-copy/grant-request-copy.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: ':token',
|
||||
resolve: {
|
||||
request: RequestCopyResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: GrantDenyRequestCopyComponent,
|
||||
},
|
||||
{
|
||||
path: REQUEST_COPY_DENY_PATH,
|
||||
component: DenyRequestCopyComponent,
|
||||
},
|
||||
{
|
||||
path: REQUEST_COPY_GRANT_PATH,
|
||||
component: GrantRequestCopyComponent,
|
||||
},
|
||||
]
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
RequestCopyResolver,
|
||||
GrantDenyRequestCopyComponent
|
||||
]
|
||||
})
|
||||
export class RequestCopyRoutingModule {
|
||||
}
|
30
src/app/request-copy/request-copy.module.ts
Normal file
30
src/app/request-copy/request-copy.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component';
|
||||
import { RequestCopyRoutingModule } from './request-copy-routing.module';
|
||||
import { DenyRequestCopyComponent } from './deny-request-copy/deny-request-copy.component';
|
||||
import { EmailRequestCopyComponent } from './email-request-copy/email-request-copy.component';
|
||||
import { GrantRequestCopyComponent } from './grant-request-copy/grant-request-copy.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
RequestCopyRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
GrantDenyRequestCopyComponent,
|
||||
DenyRequestCopyComponent,
|
||||
EmailRequestCopyComponent,
|
||||
GrantRequestCopyComponent,
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
|
||||
/**
|
||||
* Module related to components used to grant or deny an item request
|
||||
*/
|
||||
export class RequestCopyModule {
|
||||
|
||||
}
|
26
src/app/request-copy/request-copy.resolver.ts
Normal file
26
src/app/request-copy/request-copy.resolver.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { ItemRequest } from '../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { ItemRequestDataService } from '../core/data/item-request-data.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
|
||||
/**
|
||||
* Resolves an {@link ItemRequest} from the token found in the route's parameters
|
||||
*/
|
||||
@Injectable()
|
||||
export class RequestCopyResolver implements Resolve<RemoteData<ItemRequest>> {
|
||||
|
||||
constructor(
|
||||
private itemRequestDataService: ItemRequestDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<ItemRequest>> | Promise<RemoteData<ItemRequest>> | RemoteData<ItemRequest> {
|
||||
return this.itemRequestDataService.findById(route.params.token).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
<div class="container">
|
||||
<h3 class="mb-4">{{'bitstream-request-a-copy.header' | translate}}</h3>
|
||||
<div *ngIf="canDownload$|async" class="alert alert-success">
|
||||
<span>{{'bitstream-request-a-copy.alert.canDownload1' | translate}}</span>
|
||||
<a [routerLink]="getBitstreamLink()">{{'bitstream-request-a-copy.alert.canDownload2'| translate}}</a>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{'bitstream-request-a-copy.intro' | translate}} <a [routerLink]="getItemPath()">{{itemName}}</a></p>
|
||||
<p *ngIf="bitstream != undefined && allfiles.value === 'false'">{{'bitstream-request-a-copy.intro.bitstream.one' | translate}} {{bitstreamName}}</p>
|
||||
<p *ngIf="allfiles.value === 'true'">{{'bitstream-request-a-copy.intro.bitstream.all' | translate}}</p>
|
||||
</div>
|
||||
<form [class]="'ng-invalid'" [formGroup]="requestCopyForm" (ngSubmit)="onSubmit()">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<label for="name">{{'bitstream-request-a-copy.name.label' | translate}}</label>
|
||||
<input [className]="(name.invalid) && (name.dirty || name.touched) ? 'form-control is-invalid' :'form-control'"
|
||||
type="text" id="name" formControlName="name"/>
|
||||
<div *ngIf="name.invalid && (name.dirty || name.touched)"
|
||||
class="invalid-feedback show-feedback">
|
||||
<span *ngIf="name.errors && name.errors.required">
|
||||
{{ 'bitstream-request-a-copy.name.error' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<label
|
||||
for="email">{{'bitstream-request-a-copy.email.label' | translate}}</label>
|
||||
<input
|
||||
[className]="(email.invalid) && (email.dirty || email.touched) ? 'form-control is-invalid' :'form-control'"
|
||||
id="email" formControlName="email">
|
||||
<div *ngIf="email.invalid && (email.dirty || email.touched)"
|
||||
class="invalid-feedback show-feedback">
|
||||
<span *ngIf="email.errors">
|
||||
{{ 'bitstream-request-a-copy.email.error' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<small class="text-muted ds-hint">{{'bitstream-request-a-copy.email.hint' |translate}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div>{{'bitstream-request-a-copy.allfiles.label' |translate}}</div>
|
||||
<div class="ml-4">
|
||||
<input [className]="'form-check-input'" type="radio"
|
||||
id="allfiles-true" formControlName="allfiles" value="true">
|
||||
<label class="form-check-label"
|
||||
for="allfiles-true">{{'bitstream-request-a-copy.files-all-true.label' | translate}}</label>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<input [className]="'form-check-input'" type="radio"
|
||||
id="allfiles-false" formControlName="allfiles" value="false" [attr.disabled]="bitstream === undefined ? true : null ">
|
||||
<label class="form-check-label"
|
||||
for="allfiles-false">{{'bitstream-request-a-copy.files-all-false.label' | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<label
|
||||
for="message">{{'bitstream-request-a-copy.message.label' | translate}}</label>
|
||||
<textarea rows="5"
|
||||
[className]="'form-control'"
|
||||
id="message" formControlName="message"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 text-right">
|
||||
|
||||
<a (click)="navigateBack()" role="button" class="btn btn-outline-secondary mr-1">
|
||||
<i class="fas fa-arrow-left"></i> {{'bitstream-request-a-copy.return' | translate}}
|
||||
</a>
|
||||
|
||||
<button
|
||||
[disabled]="requestCopyForm.invalid"
|
||||
class="btn btn-default btn-primary"
|
||||
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,289 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../remote-data.utils';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterStub } from '../testing/router.stub';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NotificationsServiceStub } from '../testing/notifications-service.stub';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../mocks/dso-name.service.mock';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Location } from '@angular/common';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
|
||||
|
||||
describe('BitstreamRequestACopyPageComponent', () => {
|
||||
let component: BitstreamRequestACopyPageComponent;
|
||||
let fixture: ComponentFixture<BitstreamRequestACopyPageComponent>;
|
||||
|
||||
let authService: AuthService;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let activatedRoute;
|
||||
let router;
|
||||
let itemRequestDataService;
|
||||
let notificationsService;
|
||||
let location;
|
||||
let bitstreamDataService;
|
||||
|
||||
let item: Item;
|
||||
let bitstream: Bitstream;
|
||||
let eperson;
|
||||
|
||||
function init() {
|
||||
eperson = Object.assign(new EPerson(), {
|
||||
email: 'test@mail.org',
|
||||
metadata: {
|
||||
'eperson.firstname': [{value: 'Test'}],
|
||||
'eperson.lastname': [{value: 'User'}],
|
||||
}
|
||||
});
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(false),
|
||||
getAuthenticatedUserFromStore: observableOf(eperson)
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationSerivice', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
|
||||
itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
|
||||
requestACopy: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
|
||||
location = jasmine.createSpyObj('location', {
|
||||
back: {}
|
||||
});
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
item = Object.assign(new Item(), {uuid: 'item-uuid'});
|
||||
|
||||
bitstream = Object.assign(new Bitstream(), {
|
||||
uuid: 'bitstreamUuid',
|
||||
_links: {
|
||||
content: {href: 'bitstream-content-link'},
|
||||
self: {href: 'bitstream-self-link'},
|
||||
}
|
||||
});
|
||||
|
||||
activatedRoute = {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(
|
||||
item
|
||||
)
|
||||
}),
|
||||
queryParams: observableOf({
|
||||
bitstream : bitstream.uuid
|
||||
})
|
||||
};
|
||||
|
||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(bitstream)
|
||||
});
|
||||
|
||||
router = new RouterStub();
|
||||
}
|
||||
|
||||
function initTestbed() {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
|
||||
declarations: [BitstreamRequestACopyPageComponent],
|
||||
providers: [
|
||||
{provide: Location, useValue: location},
|
||||
{provide: ActivatedRoute, useValue: activatedRoute},
|
||||
{provide: Router, useValue: router},
|
||||
{provide: AuthorizationDataService, useValue: authorizationService},
|
||||
{provide: AuthService, useValue: authService},
|
||||
{provide: ItemRequestDataService, useValue: itemRequestDataService},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: DSONameService, useValue: new DSONameServiceMock()},
|
||||
{provide: BitstreamDataService, useValue: bitstreamDataService},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the comp', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should show a form to request a copy', () => {
|
||||
describe('when the user is not logged in', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('show the form with no values filled in based on the user', () => {
|
||||
expect(component.name.value).toEqual('');
|
||||
expect(component.email.value).toEqual('');
|
||||
expect(component.allfiles.value).toEqual('false');
|
||||
expect(component.message.value).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is logged in', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('show the form with values filled in based on the user', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.name.value).toEqual(eperson.name);
|
||||
expect(component.email.value).toEqual(eperson.email);
|
||||
expect(component.allfiles.value).toEqual('false');
|
||||
expect(component.message.value).toEqual('');
|
||||
});
|
||||
});
|
||||
describe('when no bitstream was provided', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
activatedRoute = {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(
|
||||
item
|
||||
)
|
||||
}),
|
||||
queryParams: observableOf({
|
||||
})
|
||||
};
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should set the all files value to true and disable the false value', () => {
|
||||
expect(component.name.value).toEqual('');
|
||||
expect(component.email.value).toEqual('');
|
||||
expect(component.allfiles.value).toEqual('true');
|
||||
expect(component.message.value).toEqual('');
|
||||
|
||||
const allFilesFalse = fixture.debugElement.query(By.css('#allfiles-false')).nativeElement;
|
||||
expect(allFilesFalse.getAttribute('disabled')).toBeTruthy();
|
||||
|
||||
});
|
||||
});
|
||||
describe('when the user has authorization to download the file', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should show an alert indicating the user can download the file', () => {
|
||||
const alert = fixture.debugElement.query(By.css('.alert')).nativeElement;
|
||||
expect(alert.innerHTML).toContain('bitstream-request-a-copy.alert.canDownload');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmit', () => {
|
||||
describe('onSuccess', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should take the current form information and submit it', () => {
|
||||
component.name.patchValue('User Name');
|
||||
component.email.patchValue('user@name.org');
|
||||
component.allfiles.patchValue('false');
|
||||
component.message.patchValue('I would like to request a copy');
|
||||
|
||||
component.onSubmit();
|
||||
const itemRequest = Object.assign(new ItemRequest(),
|
||||
{
|
||||
itemId: item.uuid,
|
||||
bitstreamId: bitstream.uuid,
|
||||
allfiles: 'false',
|
||||
requestEmail: 'user@name.org',
|
||||
requestName: 'User Name',
|
||||
requestMessage: 'I would like to request a copy'
|
||||
});
|
||||
|
||||
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(location.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFail', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
(itemRequestDataService.requestACopy as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should take the current form information and submit it', () => {
|
||||
component.name.patchValue('User Name');
|
||||
component.email.patchValue('user@name.org');
|
||||
component.allfiles.patchValue('false');
|
||||
component.message.patchValue('I would like to request a copy');
|
||||
|
||||
component.onSubmit();
|
||||
const itemRequest = Object.assign(new ItemRequest(),
|
||||
{
|
||||
itemId: item.uuid,
|
||||
bitstreamId: bitstream.uuid,
|
||||
allfiles: 'false',
|
||||
requestEmail: 'user@name.org',
|
||||
requestName: 'User Name',
|
||||
requestMessage: 'I would like to request a copy'
|
||||
});
|
||||
|
||||
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest);
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(location.back).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,213 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { getBitstreamDownloadRoute, getForbiddenRoute } from '../../app-routing-paths';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { Location } from '@angular/common';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-bitstream-request-a-copy-page',
|
||||
templateUrl: './bitstream-request-a-copy-page.component.html'
|
||||
})
|
||||
/**
|
||||
* Page component for requesting a copy for a bitstream
|
||||
*/
|
||||
export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
item$: Observable<Item>;
|
||||
|
||||
canDownload$: Observable<boolean>;
|
||||
private subs: Subscription[] = [];
|
||||
requestCopyForm: FormGroup;
|
||||
|
||||
item: Item;
|
||||
itemName: string;
|
||||
|
||||
bitstream$: Observable<Bitstream>;
|
||||
bitstream: Bitstream;
|
||||
bitstreamName: string;
|
||||
|
||||
constructor(private location: Location,
|
||||
private translateService: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private auth: AuthService,
|
||||
private formBuilder: FormBuilder,
|
||||
private itemRequestDataService: ItemRequestDataService,
|
||||
private notificationsService: NotificationsService,
|
||||
private dsoNameService: DSONameService,
|
||||
private bitstreamService: BitstreamDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.requestCopyForm = this.formBuilder.group({
|
||||
name: new FormControl('', {
|
||||
validators: [Validators.required],
|
||||
}),
|
||||
email: new FormControl('', {
|
||||
validators: [Validators.required,
|
||||
Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$')]
|
||||
}),
|
||||
allfiles: new FormControl(''),
|
||||
message: new FormControl(''),
|
||||
});
|
||||
|
||||
|
||||
this.item$ = this.route.data.pipe(
|
||||
map((data) => data.dso),
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
);
|
||||
|
||||
this.subs.push(this.item$.subscribe((item) => {
|
||||
this.item = item;
|
||||
this.itemName = this.dsoNameService.getName(item);
|
||||
}));
|
||||
|
||||
this.bitstream$ = this.route.queryParams.pipe(
|
||||
filter((params) => hasValue(params) && hasValue(params.bitstream)),
|
||||
switchMap((params) => this.bitstreamService.findById(params.bitstream)),
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
);
|
||||
|
||||
this.subs.push(this.bitstream$.subscribe((bitstream) => {
|
||||
this.bitstream = bitstream;
|
||||
this.bitstreamName = this.dsoNameService.getName(bitstream);
|
||||
}));
|
||||
|
||||
this.canDownload$ = this.bitstream$.pipe(
|
||||
switchMap((bitstream) => this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(bitstream) ? bitstream.self : undefined))
|
||||
);
|
||||
const canRequestCopy$ = this.bitstream$.pipe(
|
||||
switchMap((bitstream) => this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(bitstream) ? bitstream.self : undefined)),
|
||||
);
|
||||
|
||||
this.subs.push(observableCombineLatest([this.canDownload$, canRequestCopy$]).subscribe(([canDownload, canRequestCopy]) => {
|
||||
if (!canDownload && !canRequestCopy) {
|
||||
this.router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true});
|
||||
}
|
||||
}));
|
||||
this.initValues();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.requestCopyForm.get('name');
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this.requestCopyForm.get('email');
|
||||
}
|
||||
|
||||
get message() {
|
||||
return this.requestCopyForm.get('message');
|
||||
}
|
||||
|
||||
get allfiles() {
|
||||
return this.requestCopyForm.get('allfiles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the form values based on the current user.
|
||||
*/
|
||||
private initValues() {
|
||||
this.getCurrentUser().pipe(take(1)).subscribe((user) => {
|
||||
this.requestCopyForm.patchValue({allfiles: 'true'});
|
||||
if (hasValue(user)) {
|
||||
this.requestCopyForm.patchValue({name: user.name, email: user.email});
|
||||
}
|
||||
});
|
||||
this.bitstream$.pipe(take(1)).subscribe((bitstream) => {
|
||||
this.requestCopyForm.patchValue({allfiles: 'false'});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current user
|
||||
*/
|
||||
private getCurrentUser(): Observable<EPerson> {
|
||||
return this.auth.isAuthenticated().pipe(
|
||||
switchMap((authenticated) => {
|
||||
if (authenticated) {
|
||||
return this.auth.getAuthenticatedUserFromStore();
|
||||
} else {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the the form values as an item request to the server.
|
||||
* When the submission is successful, the user will be redirected to the item page and a success notification will be shown.
|
||||
* When the submission fails, the user will stay on the page and an error notification will be shown
|
||||
*/
|
||||
onSubmit() {
|
||||
const itemRequest = new ItemRequest();
|
||||
if (hasValue(this.bitstream)) {
|
||||
itemRequest.bitstreamId = this.bitstream.uuid;
|
||||
}
|
||||
itemRequest.itemId = this.item.uuid;
|
||||
itemRequest.allfiles = this.allfiles.value;
|
||||
itemRequest.requestEmail = this.email.value;
|
||||
itemRequest.requestName = this.name.value;
|
||||
itemRequest.requestMessage = this.message.value;
|
||||
|
||||
this.itemRequestDataService.requestACopy(itemRequest).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('bitstream-request-a-copy.submit.success'));
|
||||
this.navigateBack();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('bitstream-request-a-copy.submit.error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.subs)) {
|
||||
this.subs.forEach((sub) => {
|
||||
if (hasValue(sub)) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the user's previous location
|
||||
*/
|
||||
navigateBack() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
getItemPath() {
|
||||
return [getItemPageRoute(this.item)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the link to the bistream download page
|
||||
*/
|
||||
getBitstreamLink() {
|
||||
return [getBitstreamDownloadRoute(this.bitstream)];
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
<a [href]="bitstreamPath" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
||||
<a [routerLink]="(bitstreamPath$| async)?.routerLink" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
||||
<span *ngIf="!(canDownload$ |async)"><i class="fas fa-lock"></i></span>
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
|
||||
|
@@ -1,62 +1,145 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FileDownloadLinkComponent } from './file-download-link.component';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getBitstreamModuleRoute } from '../../app-routing-paths';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getItemModuleRoute } from '../../item-page/item-page-routing-paths';
|
||||
import { RouterLinkDirectiveStub } from '../testing/router-link-directive.stub';
|
||||
|
||||
describe('FileDownloadLinkComponent', () => {
|
||||
let component: FileDownloadLinkComponent;
|
||||
let fixture: ComponentFixture<FileDownloadLinkComponent>;
|
||||
|
||||
let authService: AuthService;
|
||||
let fileService: FileService;
|
||||
let scheduler;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
|
||||
let bitstream: Bitstream;
|
||||
let item: Item;
|
||||
|
||||
function init() {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: cold('-a', {a: true})
|
||||
});
|
||||
fileService = jasmine.createSpyObj('fileService', ['downloadFile']);
|
||||
bitstream = Object.assign(new Bitstream(), {
|
||||
uuid: 'bitstreamUuid',
|
||||
_links: {
|
||||
self: {href: 'obj-selflink'}
|
||||
}
|
||||
});
|
||||
item = Object.assign(new Item(), {
|
||||
uuid: 'itemUuid',
|
||||
_links: {
|
||||
self: {href: 'obj-selflink'}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
function initTestbed() {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FileDownloadLinkComponent],
|
||||
declarations: [FileDownloadLinkComponent, RouterLinkDirectiveStub],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: FileService, useValue: fileService },
|
||||
{provide: AuthorizationDataService, useValue: authorizationService},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
|
||||
describe('getBitstreamPath', () => {
|
||||
it('should set the bitstreamPath based on the input bitstream', () => {
|
||||
expect(component.bitstreamPath).toEqual(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||
describe('when the user has download rights', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||
expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(), queryParams: {} }}));
|
||||
expect(component.canDownload$).toBeObservable(cold('--a', {a: true}));
|
||||
|
||||
});
|
||||
it('should init the component', () => {
|
||||
scheduler.flush();
|
||||
fixture.detectChanges();
|
||||
const link = fixture.debugElement.query(By.css('a'));
|
||||
expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||
const lock = fixture.debugElement.query(By.css('.fa-lock'));
|
||||
expect(lock).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('when the user has no download rights but has the right to request a copy', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
init();
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.callFake((featureId, object) => {
|
||||
if (featureId === FeatureID.CanDownload) {
|
||||
return cold('-a', {a: false});
|
||||
}
|
||||
return cold('-a', {a: true});
|
||||
});
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
component.bitstream = bitstream;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||
expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString(), queryParams: { bitstream: bitstream.uuid } }}));
|
||||
expect(component.canDownload$).toBeObservable(cold('--a', {a: false}));
|
||||
|
||||
});
|
||||
it('should init the component', () => {
|
||||
scheduler.flush();
|
||||
fixture.detectChanges();
|
||||
const link = fixture.debugElement.query(By.css('a'));
|
||||
expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString());
|
||||
const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement;
|
||||
expect(lock).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('when the user has no download rights and no request a copy rights', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
init();
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(cold('-a', {a: false}));
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
component.item = item;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||
expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(), queryParams: {} }}));
|
||||
expect(component.canDownload$).toBeObservable(cold('--a', {a: false}));
|
||||
|
||||
});
|
||||
it('should init the component', () => {
|
||||
scheduler.flush();
|
||||
fixture.detectChanges();
|
||||
const link = fixture.debugElement.query(By.css('a'));
|
||||
expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||
const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement;
|
||||
expect(lock).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should init the component', () => {
|
||||
const link = fixture.debugElement.query(By.css('a')).nativeElement;
|
||||
expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,12 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||
import { getBitstreamDownloadRoute, getBitstreamRequestACopyRoute } from '../../app-routing-paths';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-file-download-link',
|
||||
@@ -19,6 +25,8 @@ export class FileDownloadLinkComponent implements OnInit {
|
||||
*/
|
||||
@Input() bitstream: Bitstream;
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* Additional css classes to apply to link
|
||||
*/
|
||||
@@ -29,13 +37,44 @@ export class FileDownloadLinkComponent implements OnInit {
|
||||
*/
|
||||
@Input() isBlank = false;
|
||||
|
||||
bitstreamPath: string;
|
||||
@Input() enableRequestACopy = true;
|
||||
|
||||
bitstreamPath$: Observable<{
|
||||
routerLink: string,
|
||||
queryParams: any,
|
||||
}>;
|
||||
|
||||
canDownload$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.bitstreamPath = this.getBitstreamPath();
|
||||
if (this.enableRequestACopy) {
|
||||
this.canDownload$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||
const canRequestACopy$ = this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||
this.bitstreamPath$ = observableCombineLatest([this.canDownload$, canRequestACopy$]).pipe(
|
||||
map(([canDownload, canRequestACopy]) => this.getBitstreamPath(canDownload, canRequestACopy))
|
||||
);
|
||||
} else {
|
||||
this.bitstreamPath$ = observableOf(this.getBitstreamDownloadPath());
|
||||
this.canDownload$ = observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
getBitstreamPath() {
|
||||
return getBitstreamDownloadRoute(this.bitstream);
|
||||
getBitstreamPath(canDownload: boolean, canRequestACopy: boolean) {
|
||||
if (!canDownload && canRequestACopy && hasValue(this.item)) {
|
||||
return getBitstreamRequestACopyRoute(this.item, this.bitstream);
|
||||
}
|
||||
return this.getBitstreamDownloadPath();
|
||||
}
|
||||
|
||||
getBitstreamDownloadPath() {
|
||||
return {
|
||||
routerLink: getBitstreamDownloadRoute(this.bitstream),
|
||||
queryParams: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -234,6 +234,7 @@ import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component
|
||||
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
||||
import { SearchNavbarComponent } from '../search-navbar/search-navbar.component';
|
||||
import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component';
|
||||
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
|
||||
/**
|
||||
* Declaration needed to make sure all decorator functions are called in time
|
||||
@@ -432,6 +433,7 @@ const COMPONENTS = [
|
||||
GroupSearchBoxComponent,
|
||||
FileDownloadLinkComponent,
|
||||
BitstreamDownloadPageComponent,
|
||||
BitstreamRequestACopyPageComponent,
|
||||
CollectionDropdownComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent,
|
||||
@@ -514,6 +516,7 @@ const ENTRY_COMPONENTS = [
|
||||
CollectionDropdownComponent,
|
||||
FileDownloadLinkComponent,
|
||||
BitstreamDownloadPageComponent,
|
||||
BitstreamRequestACopyPageComponent,
|
||||
CurationFormComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent,
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<div class="float-right w-15" [class.sticky-buttons]="!readMode">
|
||||
<ng-container *ngIf="readMode">
|
||||
<ds-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()">
|
||||
<ds-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()" [enableRequestACopy]="false">
|
||||
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
|
||||
</ds-file-download-link>
|
||||
<button class="btn btn-link-focus"
|
||||
|
@@ -588,6 +588,43 @@
|
||||
|
||||
"bitstream.edit.title": "Edit bitstream",
|
||||
|
||||
"bitstream-request-a-copy.alert.canDownload1": "You already have access to this file. If you want to download the file, click ",
|
||||
|
||||
"bitstream-request-a-copy.alert.canDownload2": "here",
|
||||
|
||||
"bitstream-request-a-copy.header": "Request a copy of the file",
|
||||
|
||||
"bitstream-request-a-copy.intro": "Enter the following information to request a copy for the following item: ",
|
||||
|
||||
"bitstream-request-a-copy.intro.bitstream.one": "Requesting the following file: ",
|
||||
"bitstream-request-a-copy.intro.bitstream.all": "Requesting all files. ",
|
||||
|
||||
"bitstream-request-a-copy.name.label": "Name *",
|
||||
|
||||
"bitstream-request-a-copy.name.error": "The name is required",
|
||||
|
||||
"bitstream-request-a-copy.email.label": "Your e-mail address *",
|
||||
|
||||
"bitstream-request-a-copy.email.hint": "This email address is used for sending the file.",
|
||||
|
||||
"bitstream-request-a-copy.email.error": "Please enter a valid email address.",
|
||||
|
||||
"bitstream-request-a-copy.allfiles.label": "Files",
|
||||
|
||||
"bitstream-request-a-copy.files-all-false.label": "Only the requested file",
|
||||
|
||||
"bitstream-request-a-copy.files-all-true.label": "All files (of this item) in restricted access",
|
||||
|
||||
"bitstream-request-a-copy.message.label": "Message",
|
||||
|
||||
"bitstream-request-a-copy.return": "Back",
|
||||
|
||||
"bitstream-request-a-copy.submit": "Request copy",
|
||||
|
||||
"bitstream-request-a-copy.submit.success": "The item request was submitted successfully.",
|
||||
|
||||
"bitstream-request-a-copy.submit.error": "Something went wrong with submitting the item request.",
|
||||
|
||||
|
||||
|
||||
"browse.comcol.by.author": "By Author",
|
||||
@@ -1193,6 +1230,20 @@
|
||||
|
||||
|
||||
|
||||
"deny-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I regret to inform you that it's not possible to send you a copy of the file(s) you have requested, concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>",
|
||||
|
||||
"deny-request-copy.email.subject": "Request copy of document",
|
||||
|
||||
"deny-request-copy.error": "An error occurred",
|
||||
|
||||
"deny-request-copy.header": "Deny document copy request",
|
||||
|
||||
"deny-request-copy.intro": "This message will be sent to the applicant of the request",
|
||||
|
||||
"deny-request-copy.success": "Successfully denied item request",
|
||||
|
||||
|
||||
|
||||
"dso.name.untitled": "Untitled",
|
||||
|
||||
|
||||
@@ -1428,6 +1479,53 @@
|
||||
"form.repeatable.sort.tip": "Drop the item in the new position",
|
||||
|
||||
|
||||
|
||||
"grant-deny-request-copy.deny": "Don't send copy",
|
||||
|
||||
"grant-deny-request-copy.email.back": "Back",
|
||||
|
||||
"grant-deny-request-copy.email.message": "Message",
|
||||
|
||||
"grant-deny-request-copy.email.message.empty": "Please enter a message",
|
||||
|
||||
"grant-deny-request-copy.email.permissions.info": "You may use this occasion to reconsider the access restrictions on the document, to avoid having to respond to these requests. If you’d like to ask the repository administrators to remove these restrictions, please check the box below.",
|
||||
|
||||
"grant-deny-request-copy.email.permissions.label": "Change to open access",
|
||||
|
||||
"grant-deny-request-copy.email.send": "Send",
|
||||
|
||||
"grant-deny-request-copy.email.subject": "Subject",
|
||||
|
||||
"grant-deny-request-copy.email.subject.empty": "Please enter a subject",
|
||||
|
||||
"grant-deny-request-copy.grant": "Send copy",
|
||||
|
||||
"grant-deny-request-copy.header": "Document copy request",
|
||||
|
||||
"grant-deny-request-copy.home-page": "Take me to the home page",
|
||||
|
||||
"grant-deny-request-copy.intro1": "If you are one of the authors of the document <a href='{{ url }}'>{{ name }}</a>, then please use one of the options below to respond to the user's request.",
|
||||
|
||||
"grant-deny-request-copy.intro2": "After choosing an option, you will be presented with a suggested email reply which you may edit.",
|
||||
|
||||
"grant-deny-request-copy.processed": "This request has already been processed. You can use the button below to get back to the home page.",
|
||||
|
||||
|
||||
|
||||
"grant-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I have the pleasure to send you in attachment a copy of the file(s) concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>",
|
||||
|
||||
"grant-request-copy.email.subject": "Request copy of document",
|
||||
|
||||
"grant-request-copy.error": "An error occurred",
|
||||
|
||||
"grant-request-copy.header": "Grant document copy request",
|
||||
|
||||
"grant-request-copy.intro": "This message will be sent to the applicant of the request. The requested document(s) will be attached.",
|
||||
|
||||
"grant-request-copy.success": "Successfully granted item request",
|
||||
|
||||
|
||||
|
||||
"home.description": "",
|
||||
|
||||
"home.breadcrumbs": "Home",
|
||||
|
Reference in New Issue
Block a user