mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Request-a-copy: Refactor for angular control flow changes
This commit is contained in:
@@ -1,10 +1,17 @@
|
|||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||||
|
import { MockBitstream1 } from '../../shared/mocks/item.mock';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ItemRequest } from '../shared/item-request.model';
|
import { ItemRequest } from '../shared/item-request.model';
|
||||||
|
import { ConfigurationDataService } from './configuration-data.service';
|
||||||
|
import { AuthorizationDataService } from './feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from './feature-authorization/feature-id';
|
||||||
|
import { FindListOptions } from './find-list-options.model';
|
||||||
import { ItemRequestDataService } from './item-request-data.service';
|
import { ItemRequestDataService } from './item-request-data.service';
|
||||||
import { PostRequest } from './request.models';
|
import { PostRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -16,12 +23,36 @@ describe('ItemRequestDataService', () => {
|
|||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
|
let configService: ConfigurationDataService;
|
||||||
|
let authorizationDataService: AuthorizationDataService;
|
||||||
|
|
||||||
const restApiEndpoint = 'rest/api/endpoint/';
|
const restApiEndpoint = 'rest/api/endpoint/';
|
||||||
const requestId = 'request-id';
|
const requestId = 'request-id';
|
||||||
let itemRequest: ItemRequest;
|
let itemRequest: ItemRequest;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configService = jasmine.createSpyObj('ConfigurationDataService', ['findByPropertyName']);
|
||||||
|
(configService.findByPropertyName as jasmine.Spy).and.callFake((propertyName: string) => {
|
||||||
|
switch (propertyName) {
|
||||||
|
case 'request.item.create.captcha':
|
||||||
|
return createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
|
name: 'request.item.create.captcha',
|
||||||
|
values: ['true'],
|
||||||
|
}));
|
||||||
|
case 'request.item.grant.link.period':
|
||||||
|
return createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
|
name: 'request.item.grant.link.period',
|
||||||
|
values: ['3600', '7200', '86400'],
|
||||||
|
}));
|
||||||
|
default:
|
||||||
|
return createSuccessfulRemoteDataObject$(new ConfigurationProperty());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
authorizationDataService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(false),
|
||||||
|
});
|
||||||
itemRequest = Object.assign(new ItemRequest(), {
|
itemRequest = Object.assign(new ItemRequest(), {
|
||||||
token: 'item-request-token',
|
token: 'item-request-token',
|
||||||
});
|
});
|
||||||
@@ -36,13 +67,42 @@ describe('ItemRequestDataService', () => {
|
|||||||
getEndpoint: observableOf(restApiEndpoint),
|
getEndpoint: observableOf(restApiEndpoint),
|
||||||
});
|
});
|
||||||
|
|
||||||
service = new ItemRequestDataService(requestService, rdbService, null, halService);
|
service = new ItemRequestDataService(requestService, rdbService, null, halService, configService, authorizationDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchBy', () => {
|
||||||
|
it('should use searchData to perform search operations', () => {
|
||||||
|
const searchMethod = 'testMethod';
|
||||||
|
const options = new FindListOptions();
|
||||||
|
|
||||||
|
const searchDataSpy = spyOn((service as any).searchData, 'searchBy').and.returnValue(observableOf(null));
|
||||||
|
|
||||||
|
service.searchBy(searchMethod, options);
|
||||||
|
|
||||||
|
expect(searchDataSpy).toHaveBeenCalledWith(
|
||||||
|
searchMethod,
|
||||||
|
options,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('requestACopy', () => {
|
describe('requestACopy', () => {
|
||||||
it('should send a POST request containing the provided item request', (done) => {
|
it('should send a POST request containing the provided item request', (done) => {
|
||||||
service.requestACopy(itemRequest).subscribe(() => {
|
const captchaPayload = 'payload';
|
||||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestId, restApiEndpoint, itemRequest));
|
service.requestACopy(itemRequest, captchaPayload).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(
|
||||||
|
new PostRequest(
|
||||||
|
requestId,
|
||||||
|
restApiEndpoint,
|
||||||
|
itemRequest,
|
||||||
|
{
|
||||||
|
headers: new HttpHeaders().set('x-captcha-payload', captchaPayload),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -59,11 +119,16 @@ describe('ItemRequestDataService', () => {
|
|||||||
service.grant(itemRequest.token, email, true).subscribe(() => {
|
service.grant(itemRequest.token, email, true).subscribe(() => {
|
||||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
method: RestRequestMethod.PUT,
|
method: RestRequestMethod.PUT,
|
||||||
|
href: `${restApiEndpoint}/${itemRequest.token}`,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
acceptRequest: true,
|
acceptRequest: true,
|
||||||
responseMessage: email.message,
|
responseMessage: email.message,
|
||||||
subject: email.subject,
|
subject: email.subject,
|
||||||
suggestOpenAccess: true,
|
suggestOpenAccess: true,
|
||||||
|
accessPeriod: 0,
|
||||||
|
}),
|
||||||
|
options: jasmine.objectContaining({
|
||||||
|
headers: jasmine.any(HttpHeaders),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
done();
|
done();
|
||||||
@@ -82,15 +147,70 @@ describe('ItemRequestDataService', () => {
|
|||||||
service.deny(itemRequest.token, email).subscribe(() => {
|
service.deny(itemRequest.token, email).subscribe(() => {
|
||||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
method: RestRequestMethod.PUT,
|
method: RestRequestMethod.PUT,
|
||||||
|
href: `${restApiEndpoint}/${itemRequest.token}`,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
acceptRequest: false,
|
acceptRequest: false,
|
||||||
responseMessage: email.message,
|
responseMessage: email.message,
|
||||||
subject: email.subject,
|
subject: email.subject,
|
||||||
suggestOpenAccess: false,
|
suggestOpenAccess: false,
|
||||||
|
accessPeriod: 0,
|
||||||
|
}),
|
||||||
|
options: jasmine.objectContaining({
|
||||||
|
headers: jasmine.any(HttpHeaders),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('requestACopy', () => {
|
||||||
|
it('should send a POST request containing the provided item request', (done) => {
|
||||||
|
const captchaPayload = 'payload';
|
||||||
|
service.requestACopy(itemRequest, captchaPayload).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(
|
||||||
|
new PostRequest(
|
||||||
|
requestId,
|
||||||
|
restApiEndpoint,
|
||||||
|
itemRequest,
|
||||||
|
{
|
||||||
|
headers: new HttpHeaders().set('x-captcha-payload', captchaPayload),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfiguredAccessPeriods', () => {
|
||||||
|
it('should return parsed integer values from config', () => {
|
||||||
|
service.getConfiguredAccessPeriods().subscribe(periods => {
|
||||||
|
expect(periods).toEqual([3600, 7200, 86400]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isProtectedByCaptcha', () => {
|
||||||
|
it('should return true when config value is "true"', () => {
|
||||||
|
const mockConfigProperty = {
|
||||||
|
name: 'request.item.create.captcha',
|
||||||
|
values: ['true'],
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
service.isProtectedByCaptcha().subscribe(result => {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canDownload', () => {
|
||||||
|
it('should check authorization for bitstream download', () => {
|
||||||
|
service.canDownload(MockBitstream1).subscribe(result => {
|
||||||
|
expect(authorizationDataService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanDownload, MockBitstream1.self);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -13,14 +13,27 @@ import {
|
|||||||
hasValue,
|
hasValue,
|
||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
} from '../../shared/empty.util';
|
} from '../../shared/empty.util';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ItemRequest } from '../shared/item-request.model';
|
import { ItemRequest } from '../shared/item-request.model';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { sendRequest } from '../shared/request.operators';
|
import { sendRequest } from '../shared/request.operators';
|
||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
|
import {
|
||||||
|
SearchData,
|
||||||
|
SearchDataImpl,
|
||||||
|
} from './base/search-data';
|
||||||
|
import { ConfigurationDataService } from './configuration-data.service';
|
||||||
|
import { AuthorizationDataService } from './feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from './feature-authorization/feature-id';
|
||||||
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import {
|
import {
|
||||||
PostRequest,
|
PostRequest,
|
||||||
@@ -34,14 +47,21 @@ import { RequestService } from './request.service';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ItemRequestDataService extends IdentifiableDataService<ItemRequest> {
|
export class ItemRequestDataService extends IdentifiableDataService<ItemRequest> implements SearchData<ItemRequest> {
|
||||||
|
|
||||||
|
// TODO: This is only public for access by the test class - smell?
|
||||||
|
private searchData: SearchDataImpl<ItemRequest>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
|
protected configService: ConfigurationDataService,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
) {
|
) {
|
||||||
super('itemrequests', requestService, rdbService, objectCache, halService);
|
super('itemrequests', requestService, rdbService, objectCache, halService);
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemRequestEndpoint(): Observable<string> {
|
getItemRequestEndpoint(): Observable<string> {
|
||||||
@@ -61,17 +81,26 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
|
|||||||
/**
|
/**
|
||||||
* Request a copy of an item
|
* Request a copy of an item
|
||||||
* @param itemRequest
|
* @param itemRequest
|
||||||
|
* @param captchaPayload payload of captcha verification
|
||||||
*/
|
*/
|
||||||
requestACopy(itemRequest: ItemRequest): Observable<RemoteData<ItemRequest>> {
|
requestACopy(itemRequest: ItemRequest, captchaPayload: string): Observable<RemoteData<ItemRequest>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const href$ = this.getItemRequestEndpoint();
|
const href$ = this.getItemRequestEndpoint();
|
||||||
|
|
||||||
|
// Inject captcha payload into headers
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
if (captchaPayload) {
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.set('x-captcha-payload', captchaPayload);
|
||||||
|
options.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
href$.pipe(
|
href$.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PostRequest(requestId, href, itemRequest);
|
const request = new PostRequest(requestId, href, itemRequest, options);
|
||||||
this.requestService.send(request);
|
this.requestService.send(request, false);
|
||||||
}),
|
}),
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
@@ -94,9 +123,10 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
|
|||||||
* @param token Token of the {@link ItemRequest}
|
* @param token Token of the {@link ItemRequest}
|
||||||
* @param email Email to send back to the user requesting the item
|
* @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
|
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||||
|
* @param accessPeriod How long in seconds to grant access, from the decision date (only applies to links, not attachments)
|
||||||
*/
|
*/
|
||||||
grant(token: string, email: RequestCopyEmail, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
grant(token: string, email: RequestCopyEmail, suggestOpenAccess = false, accessPeriod = 0): Observable<RemoteData<ItemRequest>> {
|
||||||
return this.process(token, email, true, suggestOpenAccess);
|
return this.process(token, email, true, suggestOpenAccess, accessPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,8 +135,9 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
|
|||||||
* @param email Email to send back to the user requesting the item
|
* @param email Email to send back to the user requesting the item
|
||||||
* @param grant Grant or deny the request (true = grant, false = deny)
|
* @param grant Grant or deny the request (true = grant, false = deny)
|
||||||
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||||
|
* @param accessPeriod How long in seconds to grant access, from the decision date (only applies to links, not attachments)
|
||||||
*/
|
*/
|
||||||
process(token: string, email: RequestCopyEmail, grant: boolean, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
process(token: string, email: RequestCopyEmail, grant: boolean, suggestOpenAccess = false, accessPeriod = 0): Observable<RemoteData<ItemRequest>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
this.getItemRequestEndpointByToken(token).pipe(
|
this.getItemRequestEndpointByToken(token).pipe(
|
||||||
@@ -121,6 +152,7 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
|
|||||||
responseMessage: email.message,
|
responseMessage: email.message,
|
||||||
subject: email.subject,
|
subject: email.subject,
|
||||||
suggestOpenAccess,
|
suggestOpenAccess,
|
||||||
|
accessPeriod: accessPeriod,
|
||||||
}), options);
|
}), options);
|
||||||
}),
|
}),
|
||||||
sendRequest(this.requestService),
|
sendRequest(this.requestService),
|
||||||
@@ -128,4 +160,102 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
|
|||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this, after discussion about implications and compare to bitstream data service byItemHandle
|
||||||
|
// Reviewers may ask that we instead just wrap the REST response in pagination even though we only expect one obj
|
||||||
|
/**
|
||||||
|
* Get a sanitized item request using the searchBy method and the access token sent to the original requester.
|
||||||
|
*
|
||||||
|
* @param accessToken access token contained in the secure link sent to a requester
|
||||||
|
*/
|
||||||
|
getSanitizedRequestByAccessTokenPaged(accessToken: string): Observable<RemoteData<PaginatedList<ItemRequest>>> {
|
||||||
|
// We only expect / want one result as access tokens are unique
|
||||||
|
const findListOptions = Object.assign({}, new FindListOptions(), {
|
||||||
|
elementsPerPage: 1,
|
||||||
|
currentPage: 1,
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('accessToken', accessToken),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Pipe the paginated searchBy results and return a single item request
|
||||||
|
return this.searchBy('byAccessToken', findListOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a sanitized item request using the searchBy method and the access token sent to the original requester.
|
||||||
|
*
|
||||||
|
* @param accessToken access token contained in the secure link sent to a requester
|
||||||
|
*/
|
||||||
|
getSanitizedRequestByAccessToken(accessToken: string): Observable<RemoteData<ItemRequest>> {
|
||||||
|
const findListOptions = Object.assign({}, new FindListOptions(), {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('accessToken', accessToken),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const hrefObs = this.getSearchByHref(
|
||||||
|
'byAccessToken',
|
||||||
|
findListOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.searchData.findByHref(
|
||||||
|
hrefObs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<ItemRequest>[]): Observable<RemoteData<PaginatedList<ItemRequest>>> {
|
||||||
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get configured access periods (in seconds) to populate the dropdown in the item request approval form
|
||||||
|
* if the 'send secure link' feature is configured.
|
||||||
|
* Expects integer values, conversion to number is done in this processing
|
||||||
|
*/
|
||||||
|
getConfiguredAccessPeriods(): Observable<number[]> {
|
||||||
|
return this.configService.findByPropertyName('request.item.grant.link.period').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : []),
|
||||||
|
map((values) => values.map(value => parseInt(value, 10))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the request copy form protected by a captcha? This will be used to decide whether to render the captcha
|
||||||
|
* component in bitstream-request-a-copy-page component
|
||||||
|
*/
|
||||||
|
isProtectedByCaptcha(): Observable<boolean> {
|
||||||
|
return this.configService.findByPropertyName('request.item.create.captcha').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return rd.payload.values.length > 0 && rd.payload.values[0] === 'true';
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object's search method with given options object
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @return {Observable<string>}
|
||||||
|
* Return an observable that emits created HREF
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<ItemRequest>[]): Observable<string> {
|
||||||
|
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization check to see if the user already has download access to the given bitstream.
|
||||||
|
* Wrapped in this service to give it a central place and make it easy to mock for testing.
|
||||||
|
*
|
||||||
|
* @param bitstream The bitstream to be downloaded
|
||||||
|
* @return {Observable<boolean>} true if user may download, false if not
|
||||||
|
*/
|
||||||
|
canDownload(bitstream: Bitstream): Observable<boolean> {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.CanDownload, bitstream?.self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -80,6 +80,16 @@ export class ItemRequest implements CacheableObject {
|
|||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
bitstreamId: string;
|
bitstreamId: string;
|
||||||
|
/**
|
||||||
|
* Access token of the request (read-only)
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
accessToken: string;
|
||||||
|
/**
|
||||||
|
* Access period of the request
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
accessPeriod: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link HALLink}s for this ItemRequest
|
* The {@link HALLink}s for this ItemRequest
|
||||||
|
@@ -84,6 +84,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Captcha - to be rendered only if enabled in backend requestitem.cfg -->
|
||||||
|
@if (!!(captchaEnabled$ | async)) {
|
||||||
|
<div *ngVar="challengeHref$ | async as href">
|
||||||
|
<ds-altcha-captcha autoload="onload" challengeUrl="{{ href }}" (payload)="handlePayload($event)">
|
||||||
|
</ds-altcha-captcha>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 text-end">
|
<div class="col-12 text-end">
|
||||||
|
@@ -16,19 +16,27 @@ import {
|
|||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface';
|
||||||
import { AuthService } from '../../../core/auth/auth.service';
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { ItemRequestDataService } from '../../../core/data/item-request-data.service';
|
import { ItemRequestDataService } from '../../../core/data/item-request-data.service';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { RequestEntry } from '../../../core/data/request-entry.model';
|
||||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||||
import { ItemRequest } from '../../../core/shared/item-request.model';
|
import { ItemRequest } from '../../../core/shared/item-request.model';
|
||||||
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {
|
import {
|
||||||
createFailedRemoteDataObject$,
|
createFailedRemoteDataObject$,
|
||||||
@@ -39,6 +47,9 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
|
|||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page.component';
|
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page.component';
|
||||||
|
|
||||||
|
const mockDataServiceMap: any = new Map([
|
||||||
|
[ITEM.value, () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService)],
|
||||||
|
]);
|
||||||
|
|
||||||
describe('BitstreamRequestACopyPageComponent', () => {
|
describe('BitstreamRequestACopyPageComponent', () => {
|
||||||
let component: BitstreamRequestACopyPageComponent;
|
let component: BitstreamRequestACopyPageComponent;
|
||||||
@@ -48,10 +59,11 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let activatedRoute;
|
let activatedRoute;
|
||||||
let router;
|
let router;
|
||||||
let itemRequestDataService;
|
let itemRequestDataService: ItemRequestDataService;
|
||||||
let notificationsService;
|
let notificationsService;
|
||||||
let location;
|
let location;
|
||||||
let bitstreamDataService;
|
let bitstreamDataService;
|
||||||
|
let requestService;
|
||||||
|
|
||||||
let item: Item;
|
let item: Item;
|
||||||
let bitstream: Bitstream;
|
let bitstream: Bitstream;
|
||||||
@@ -75,8 +87,20 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
|
|
||||||
itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
|
itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
|
||||||
requestACopy: createSuccessfulRemoteDataObject$({}),
|
requestACopy: createSuccessfulRemoteDataObject$({}),
|
||||||
|
isProtectedByCaptcha: observableOf(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
requestService = Object.assign(getMockRequestService(), {
|
||||||
|
getByHref(requestHref: string) {
|
||||||
|
const responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'OK');
|
||||||
|
return observableOf(responseCacheEntry);
|
||||||
|
},
|
||||||
|
removeByHrefSubstring(href: string) {
|
||||||
|
// Do nothing
|
||||||
|
},
|
||||||
|
}) as RequestService;
|
||||||
|
|
||||||
location = jasmine.createSpyObj('location', {
|
location = jasmine.createSpyObj('location', {
|
||||||
back: {},
|
back: {},
|
||||||
});
|
});
|
||||||
@@ -124,6 +148,9 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||||
|
{ provide: Store, useValue: provideMockStore() },
|
||||||
|
{ provide: RequestService, useValue: requestService },
|
||||||
|
{ provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
@@ -246,6 +273,7 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
component.email.patchValue('user@name.org');
|
component.email.patchValue('user@name.org');
|
||||||
component.allfiles.patchValue('false');
|
component.allfiles.patchValue('false');
|
||||||
component.message.patchValue('I would like to request a copy');
|
component.message.patchValue('I would like to request a copy');
|
||||||
|
component.captchaPayload.patchValue('payload');
|
||||||
|
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
const itemRequest = Object.assign(new ItemRequest(),
|
const itemRequest = Object.assign(new ItemRequest(),
|
||||||
@@ -258,7 +286,7 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
requestMessage: 'I would like to request a copy',
|
requestMessage: 'I would like to request a copy',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest);
|
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest, 'payload');
|
||||||
expect(notificationsService.success).toHaveBeenCalled();
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
expect(location.back).toHaveBeenCalled();
|
expect(location.back).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -280,6 +308,7 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
component.email.patchValue('user@name.org');
|
component.email.patchValue('user@name.org');
|
||||||
component.allfiles.patchValue('false');
|
component.allfiles.patchValue('false');
|
||||||
component.message.patchValue('I would like to request a copy');
|
component.message.patchValue('I would like to request a copy');
|
||||||
|
component.captchaPayload.patchValue('payload');
|
||||||
|
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
const itemRequest = Object.assign(new ItemRequest(),
|
const itemRequest = Object.assign(new ItemRequest(),
|
||||||
@@ -292,7 +321,7 @@ describe('BitstreamRequestACopyPageComponent', () => {
|
|||||||
requestMessage: 'I would like to request a copy',
|
requestMessage: 'I would like to request a copy',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest);
|
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest, 'payload');
|
||||||
expect(notificationsService.error).toHaveBeenCalled();
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
expect(location.back).not.toHaveBeenCalled();
|
expect(location.back).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
|
import 'altcha';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
Location,
|
Location,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@@ -46,6 +50,7 @@ import { BitstreamDataService } from '../../../core/data/bitstream-data.service'
|
|||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
import { ItemRequestDataService } from '../../../core/data/item-request-data.service';
|
import { ItemRequestDataService } from '../../../core/data/item-request-data.service';
|
||||||
|
import { ProofOfWorkCaptchaDataService } from '../../../core/data/proof-of-work-captcha-data.service';
|
||||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
@@ -60,7 +65,9 @@ import {
|
|||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
} from '../../../shared/empty.util';
|
} from '../../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { AltchaCaptchaComponent } from './altcha-captcha.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-bitstream-request-a-copy-page',
|
selector: 'ds-bitstream-request-a-copy-page',
|
||||||
@@ -71,7 +78,10 @@ import { getItemPageRoute } from '../../item-page-routing-paths';
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
BtnDisabledDirective,
|
BtnDisabledDirective,
|
||||||
|
VarDirective,
|
||||||
|
AltchaCaptchaComponent,
|
||||||
],
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +102,10 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
bitstream: Bitstream;
|
bitstream: Bitstream;
|
||||||
bitstreamName: string;
|
bitstreamName: string;
|
||||||
|
|
||||||
|
// Captcha settings
|
||||||
|
captchaEnabled$: Observable<boolean>;
|
||||||
|
challengeHref$: Observable<string>;
|
||||||
|
|
||||||
constructor(private location: Location,
|
constructor(private location: Location,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -103,6 +117,8 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private dsoNameService: DSONameService,
|
private dsoNameService: DSONameService,
|
||||||
private bitstreamService: BitstreamDataService,
|
private bitstreamService: BitstreamDataService,
|
||||||
|
private captchaService: ProofOfWorkCaptchaDataService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +133,15 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
allfiles: new UntypedFormControl(''),
|
allfiles: new UntypedFormControl(''),
|
||||||
message: new UntypedFormControl(''),
|
message: new UntypedFormControl(''),
|
||||||
|
// Payload here is initialised as "required", but this validator will be cleared
|
||||||
|
// if the config property comes back as 'captcha not enabled'
|
||||||
|
captchaPayload: new UntypedFormControl('', {
|
||||||
|
validators: [Validators.required],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.captchaEnabled$ = this.itemRequestDataService.isProtectedByCaptcha();
|
||||||
|
this.challengeHref$ = this.captchaService.getChallengeHref();
|
||||||
|
|
||||||
this.item$ = this.route.data.pipe(
|
this.item$ = this.route.data.pipe(
|
||||||
map((data) => data.dso),
|
map((data) => data.dso),
|
||||||
@@ -172,6 +195,10 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
return this.requestCopyForm.get('allfiles');
|
return this.requestCopyForm.get('allfiles');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get captchaPayload() {
|
||||||
|
return this.requestCopyForm.get('captchaPayload');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise the form values based on the current user.
|
* Initialise the form values based on the current user.
|
||||||
*/
|
*/
|
||||||
@@ -185,6 +212,17 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
this.bitstream$.pipe(take(1)).subscribe((bitstream) => {
|
this.bitstream$.pipe(take(1)).subscribe((bitstream) => {
|
||||||
this.requestCopyForm.patchValue({ allfiles: 'false' });
|
this.requestCopyForm.patchValue({ allfiles: 'false' });
|
||||||
});
|
});
|
||||||
|
this.subs.push(this.captchaEnabled$.pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
// Captcha not required? Clear validators to allow the form to be submitted normally
|
||||||
|
this.requestCopyForm.get('captchaPayload').clearValidators();
|
||||||
|
this.requestCopyForm.get('captchaPayload').reset();
|
||||||
|
this.requestCopyForm.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,8 +256,9 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
itemRequest.requestEmail = this.email.value;
|
itemRequest.requestEmail = this.email.value;
|
||||||
itemRequest.requestName = this.name.value;
|
itemRequest.requestName = this.name.value;
|
||||||
itemRequest.requestMessage = this.message.value;
|
itemRequest.requestMessage = this.message.value;
|
||||||
|
const captchaPayloadString: string = this.captchaPayload.value;
|
||||||
|
|
||||||
this.itemRequestDataService.requestACopy(itemRequest).pipe(
|
this.itemRequestDataService.requestACopy(itemRequest, captchaPayloadString).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
).subscribe((rd) => {
|
).subscribe((rd) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
@@ -231,6 +270,10 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePayload(event): void {
|
||||||
|
this.requestCopyForm.patchValue({ captchaPayload: event });
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
if (hasValue(this.subs)) {
|
if (hasValue(this.subs)) {
|
||||||
this.subs.forEach((sub) => {
|
this.subs.forEach((sub) => {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
<form>
|
<form>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="subject" class="form-label">{{ 'grant-deny-request-copy.email.subject' | translate }}</label>
|
<label for="subject" class="form-label">{{ '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">
|
<input type="text" class="form-control" id="subject" [ngClass]="{'is-invalid': !subject || subject.length === 0}"
|
||||||
|
[(ngModel)]="subject" name="subject">
|
||||||
@if (!subject || subject.length === 0) {
|
@if (!subject || subject.length === 0) {
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{ 'grant-deny-request-copy.email.subject.empty' | translate }}
|
{{ 'grant-deny-request-copy.email.subject.empty' | translate }}
|
||||||
@@ -12,18 +13,46 @@
|
|||||||
<label for="message" class="form-label">{{ 'grant-deny-request-copy.email.message' | translate }}</label>
|
<label for="message" class="form-label">{{ 'grant-deny-request-copy.email.message' | translate }}</label>
|
||||||
<textarea class="form-control" id="message" rows="8" [(ngModel)]="message" name="message"></textarea>
|
<textarea class="form-control" id="message" rows="8" [(ngModel)]="message" name="message"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Display access periods if more than one was bound to input. The parent component (grant-request-copy)
|
||||||
|
sends an empty list if the feature is not enabled or applicable to this request. -->
|
||||||
|
@if (hasValue(validAccessPeriods) && validAccessPeriods.length > 0) {
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="accessPeriod">{{ 'grant-request-copy.access-period.header' | translate }}</label>
|
||||||
|
<div ngbDropdown class="d-block">
|
||||||
|
<!-- Show current selected access period (defaults to first in array) -->
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary playlist"
|
||||||
|
id="accessPeriod"
|
||||||
|
ngbDropdownToggle
|
||||||
|
> {{ 'grant-request-copy.access-period.' + this.accessPeriod | translate }}
|
||||||
|
</button>
|
||||||
|
<!-- Access period dropdown -->
|
||||||
|
<div ngbDropdownMenu aria-labelledby="accessPeriod">
|
||||||
|
@for (accessPeriod of validAccessPeriods; track accessPeriod) {
|
||||||
|
<button
|
||||||
|
ngbDropdownItem
|
||||||
|
class="list-element"
|
||||||
|
(click)="selectAccessPeriod(accessPeriod)"
|
||||||
|
>
|
||||||
|
{{ ('grant-request-copy.access-period.' + accessPeriod | translate) }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
<div class="d-flex flex-row-reverse">
|
<div class="d-flex flex-row-reverse">
|
||||||
<button (click)="submit()"
|
<button (click)="submit()"
|
||||||
[dsBtnDisabled]="!subject || subject.length === 0"
|
[dsBtnDisabled]="!subject || subject.length === 0"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
||||||
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
|
<i class="fas fa-envelope"></i> {{ 'grant-deny-request-copy.email.send' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button (click)="return()"
|
<button (click)="return()"
|
||||||
class="btn btn-outline-secondary me-1"
|
class="btn btn-outline-secondary me-1"
|
||||||
title="{{'grant-deny-request-copy.email.back' | translate }}">
|
title="{{'grant-deny-request-copy.email.back' | translate }}">
|
||||||
<i class="fas fa-arrow-left"></i> {{'grant-deny-request-copy.email.back' | translate }}
|
<i class="fas fa-arrow-left"></i> {{ 'grant-deny-request-copy.email.back' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -45,6 +45,7 @@ describe('EmailRequestCopyComponent', () => {
|
|||||||
spyOn(component.send, 'emit').and.stub();
|
spyOn(component.send, 'emit').and.stub();
|
||||||
component.subject = 'test-subject';
|
component.subject = 'test-subject';
|
||||||
component.message = 'test-message';
|
component.message = 'test-message';
|
||||||
|
component.validAccessPeriods = [0];
|
||||||
component.submit();
|
component.submit();
|
||||||
expect(component.send.emit).toHaveBeenCalledWith(new RequestCopyEmail('test-subject', 'test-message'));
|
expect(component.send.emit).toHaveBeenCalledWith(new RequestCopyEmail('test-subject', 'test-message'));
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'altcha';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Location,
|
Location,
|
||||||
NgClass,
|
NgClass,
|
||||||
@@ -6,30 +8,33 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { RequestCopyEmail } from './request-copy-email.model';
|
import { RequestCopyEmail } from './request-copy-email.model';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-base-email-request-copy',
|
selector: 'ds-base-email-request-copy',
|
||||||
styleUrls: ['./email-request-copy.component.scss'],
|
styleUrls: ['./email-request-copy.component.scss'],
|
||||||
templateUrl: './email-request-copy.component.html',
|
templateUrl: './email-request-copy.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, NgClass, TranslateModule, BtnDisabledDirective],
|
imports: [FormsModule, NgClass, TranslateModule, BtnDisabledDirective, NgbDropdownModule],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* A form component for an email to send back to the user requesting an item
|
* A form component for an email to send back to the user requesting an item
|
||||||
*/
|
*/
|
||||||
export class EmailRequestCopyComponent {
|
export class EmailRequestCopyComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Event emitter for sending the email
|
* Event emitter for sending the email
|
||||||
*/
|
*/
|
||||||
@Output() send: EventEmitter<RequestCopyEmail> = new EventEmitter<RequestCopyEmail>();
|
@Output() send: EventEmitter<RequestCopyEmail> = new EventEmitter<RequestCopyEmail>();
|
||||||
|
@Output() selectedAccessPeriod: EventEmitter<number> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The subject of the email
|
* The subject of the email
|
||||||
@@ -41,9 +46,28 @@ export class EmailRequestCopyComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() message: string;
|
@Input() message: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of valid access periods to render in a drop-down menu
|
||||||
|
*/
|
||||||
|
@Input() validAccessPeriods: number[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected access period
|
||||||
|
*/
|
||||||
|
accessPeriod = 0;
|
||||||
|
|
||||||
|
protected readonly hasValue = hasValue;
|
||||||
|
|
||||||
constructor(protected location: Location) {
|
constructor(protected location: Location) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// If access periods are present, set the default to the first in the array
|
||||||
|
if (hasValue(this.validAccessPeriods) && this.validAccessPeriods.length > 0) {
|
||||||
|
this.selectAccessPeriod(this.validAccessPeriods[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit the email
|
* Submit the email
|
||||||
*/
|
*/
|
||||||
@@ -57,4 +81,14 @@ export class EmailRequestCopyComponent {
|
|||||||
return() {
|
return() {
|
||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the access period when a dropdown menu button is clicked for a value
|
||||||
|
* @param accessPeriod
|
||||||
|
*/
|
||||||
|
selectAccessPeriod(accessPeriod: number) {
|
||||||
|
this.accessPeriod = accessPeriod;
|
||||||
|
this.selectedAccessPeriod.emit(accessPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,11 @@ export class ThemedEmailRequestCopyComponent extends ThemedComponent<EmailReques
|
|||||||
*/
|
*/
|
||||||
@Output() send: EventEmitter<RequestCopyEmail> = new EventEmitter<RequestCopyEmail>();
|
@Output() send: EventEmitter<RequestCopyEmail> = new EventEmitter<RequestCopyEmail>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitter for a selected / changed access period
|
||||||
|
*/
|
||||||
|
@Output() selectedAccessPeriod: EventEmitter<number> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The subject of the email
|
* The subject of the email
|
||||||
*/
|
*/
|
||||||
@@ -35,7 +40,13 @@ export class ThemedEmailRequestCopyComponent extends ThemedComponent<EmailReques
|
|||||||
*/
|
*/
|
||||||
@Input() message: string;
|
@Input() message: string;
|
||||||
|
|
||||||
protected inAndOutputNames: (keyof EmailRequestCopyComponent & keyof this)[] = ['send', 'subject', 'message'];
|
/**
|
||||||
|
* A list of valid access periods, if configured
|
||||||
|
*/
|
||||||
|
@Input() validAccessPeriods: number[];
|
||||||
|
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof EmailRequestCopyComponent & keyof this)[] = ['send', 'subject', 'message', 'validAccessPeriods', 'selectedAccessPeriod'];
|
||||||
|
|
||||||
protected getComponentName(): string {
|
protected getComponentName(): string {
|
||||||
return 'EmailRequestCopyComponent';
|
return 'EmailRequestCopyComponent';
|
||||||
|
@@ -1,32 +1,50 @@
|
|||||||
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||||
<h3 class="mb-4">{{'grant-deny-request-copy.header' | translate}}</h3>
|
<h3 class="mb-4">{{ 'grant-deny-request-copy.header' | translate }}</h3>
|
||||||
|
|
||||||
@if (itemRequestRD && itemRequestRD.hasSucceeded) {
|
@if (itemRequestRD && itemRequestRD.hasSucceeded) {
|
||||||
<div>
|
<div>
|
||||||
@if (!itemRequestRD.payload.decisionDate) {
|
<!-- Allow previous decisions *if* they were "accept" and have an access token - this allows us to use the form to revoke access -->
|
||||||
|
@if (!itemRequestRD.payload.decisionDate || (itemRequestRD.payload.acceptRequest === true && itemRequestRD.payload.accessToken)) {
|
||||||
<div>
|
<div>
|
||||||
<p [innerHTML]="'grant-deny-request-copy.intro1' | translate:{ url: (itemUrl$ | async), name: (itemName$ | async) }"></p>
|
<p
|
||||||
<p>{{'grant-deny-request-copy.intro2' | translate}}</p>
|
[innerHTML]="'grant-deny-request-copy.intro1' | translate:{ url: (itemUrl$ | async), name: (itemName$ | async) }"></p>
|
||||||
|
<p>{{ 'grant-deny-request-copy.intro2' | translate }}</p>
|
||||||
|
@if (itemRequestRD.payload.decisionDate) {
|
||||||
|
<p>{{ 'grant-deny-request-copy.previous-decision' | translate }}</p>
|
||||||
|
}
|
||||||
<div class="btn-group ">
|
<div class="btn-group ">
|
||||||
<a [routerLink]="grantRoute$ | async"
|
<!-- Don't show accept button for previous requests, we only want to allow revoking old requests -->
|
||||||
class="btn btn-outline-primary"
|
@if (!itemRequestRD.payload.decisionDate) {
|
||||||
title="{{'grant-deny-request-copy.grant' | translate }}">
|
<a [routerLink]="grantRoute$ | async"
|
||||||
{{'grant-deny-request-copy.grant' | translate }}
|
class="btn btn-outline-primary"
|
||||||
</a>
|
title="{{'grant-deny-request-copy.grant' | translate }}">
|
||||||
<a [routerLink]="denyRoute$ | async"
|
{{ 'grant-deny-request-copy.grant' | translate }}
|
||||||
class="btn btn-outline-danger"
|
</a>
|
||||||
title="{{'grant-deny-request-copy.deny' | translate }}">
|
|
||||||
{{'grant-deny-request-copy.deny' | translate }}
|
<a [routerLink]="denyRoute$ | async"
|
||||||
</a>
|
class="btn btn-outline-danger"
|
||||||
|
title="{{'grant-deny-request-copy.deny' | translate }}">
|
||||||
|
{{ 'grant-deny-request-copy.deny' | translate }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (itemRequestRD.payload.decisionDate && itemRequestRD.payload.acceptRequest === true && itemRequestRD.payload.accessToken) {
|
||||||
|
<a [routerLink]="denyRoute$ | async"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
title="{{'grant-deny-request-copy.revoke' | translate }}">
|
||||||
|
{{ 'grant-deny-request-copy.revoke' | translate }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<!-- Display the "already handled" message if there is a decision date, and either no access token (attachment was sent in email) or the request was denied -->
|
||||||
@if (itemRequestRD.payload.decisionDate) {
|
@if (itemRequestRD.payload.decisionDate && (itemRequestRD.payload.acceptRequest === false || !itemRequestRD.payload.accessToken)) {
|
||||||
<div class="processed-message">
|
<div class="processed-message">
|
||||||
<p>{{'grant-deny-request-copy.processed' | translate}}</p>
|
<p>{{ 'grant-deny-request-copy.processed' | translate }}</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<a routerLink="/home" class="btn btn-primary">{{'grant-deny-request-copy.home-page' | translate}}</a>
|
<a routerLink="/home" class="btn btn-primary">{{ 'grant-deny-request-copy.home-page' | translate }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,37 @@
|
|||||||
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||||
<h3 class="mb-4">{{'grant-request-copy.header' | translate}}</h3>
|
<h3 class="mb-4">{{ 'grant-request-copy.header' | translate }}</h3>
|
||||||
@if (itemRequestRD && itemRequestRD.hasSucceeded) {
|
@if (itemRequestRD && itemRequestRD.hasSucceeded) {
|
||||||
<div>
|
<div>
|
||||||
<p>{{'grant-request-copy.intro' | translate}}</p>
|
<!-- Show the appropriate intro text depending on whether the email will have an attachment or a web link -->
|
||||||
<ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="grant($event)">
|
<p>{{ 'grant-request-copy.intro.' + (sendAsAttachment ? 'attachment' : 'link') | translate }}</p>
|
||||||
|
|
||||||
|
@if (!sendAsAttachment && hasValue(previewLink)) {
|
||||||
|
<div>
|
||||||
|
<p>{{ 'grant-request-copy.intro.link.preview' | translate }}
|
||||||
|
<a [attr.routerLink]="previewLinkOptions.routerLink" class="dont-break-out d-block" [target]="'_blank'"
|
||||||
|
[attr.queryParams]="previewLinkOptions.queryParams"
|
||||||
|
[attr.rel]=""
|
||||||
|
>
|
||||||
|
{{ previewLink }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Only send access periods for display if an access token was present -->
|
||||||
|
<ds-email-request-copy [subject]="subject$ | async"
|
||||||
|
[message]="message$ | async"
|
||||||
|
[validAccessPeriods]="(hasValue(itemRequestRD.payload.accessToken) ? (validAccessPeriods$ | async) : [])"
|
||||||
|
(send)="grant($event)"
|
||||||
|
(selectedAccessPeriod)="selectAccessPeriod($event)"
|
||||||
|
>
|
||||||
<p>{{ 'grant-deny-request-copy.email.permissions.info' | translate }}</p>
|
<p>{{ 'grant-deny-request-copy.email.permissions.info' | translate }}</p>
|
||||||
<form class="mb-3">
|
<form class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="permissions" [(ngModel)]="suggestOpenAccess" name="permissions">
|
<input class="form-check-input" type="checkbox" value="" id="permissions" [(ngModel)]="suggestOpenAccess"
|
||||||
<label class="form-check-label" for="permissions">{{ 'grant-deny-request-copy.email.permissions.label' | translate }}</label>
|
name="permissions">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="permissions">{{ 'grant-deny-request-copy.email.permissions.label' | translate }}</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ds-email-request-copy>
|
</ds-email-request-copy>
|
||||||
|
@@ -20,6 +20,7 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
|||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock';
|
||||||
@@ -46,6 +47,7 @@ describe('GrantRequestCopyComponent', () => {
|
|||||||
let itemDataService: ItemDataService;
|
let itemDataService: ItemDataService;
|
||||||
let itemRequestService: ItemRequestDataService;
|
let itemRequestService: ItemRequestDataService;
|
||||||
let notificationsService: NotificationsService;
|
let notificationsService: NotificationsService;
|
||||||
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
let itemRequest: ItemRequest;
|
let itemRequest: ItemRequest;
|
||||||
let user: EPerson;
|
let user: EPerson;
|
||||||
@@ -90,7 +92,6 @@ describe('GrantRequestCopyComponent', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||||
});
|
});
|
||||||
@@ -106,11 +107,17 @@ describe('GrantRequestCopyComponent', () => {
|
|||||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||||
findById: createSuccessfulRemoteDataObject$(item),
|
findById: createSuccessfulRemoteDataObject$(item),
|
||||||
});
|
});
|
||||||
itemRequestService = jasmine.createSpyObj('itemRequestService', {
|
itemRequestService = jasmine.createSpyObj('ItemRequestDataService', {
|
||||||
|
getSanitizedRequestByAccessToken: observableOf(createSuccessfulRemoteDataObject(itemRequest)),
|
||||||
grant: createSuccessfulRemoteDataObject$(itemRequest),
|
grant: createSuccessfulRemoteDataObject$(itemRequest),
|
||||||
|
getConfiguredAccessPeriods: observableOf([3600, 7200, 14400]), // Common access periods in seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
getAuthenticatedUserFromStore: observableOf(user),
|
||||||
});
|
});
|
||||||
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
|
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), GrantRequestCopyComponent, VarDirective],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), GrantRequestCopyComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -121,6 +128,7 @@ describe('GrantRequestCopyComponent', () => {
|
|||||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
import { AsyncPipe } from '@angular/common';
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
CommonModule,
|
||||||
|
NgClass,
|
||||||
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -7,6 +11,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
|
RouterLink,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
@@ -16,17 +21,21 @@ import { Observable } from 'rxjs';
|
|||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
tap,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { getAccessTokenRequestRoute } from '../../app-routing-paths';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
import {
|
import {
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
@@ -38,7 +47,7 @@ import { ThemedEmailRequestCopyComponent } from '../email-request-copy/themed-em
|
|||||||
styleUrls: ['./grant-request-copy.component.scss'],
|
styleUrls: ['./grant-request-copy.component.scss'],
|
||||||
templateUrl: './grant-request-copy.component.html',
|
templateUrl: './grant-request-copy.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [VarDirective, ThemedEmailRequestCopyComponent, FormsModule, ThemedLoadingComponent, AsyncPipe, TranslateModule],
|
imports: [CommonModule, VarDirective, NgIf, ThemedEmailRequestCopyComponent, FormsModule, ThemedLoadingComponent, AsyncPipe, TranslateModule, RouterLink, NgClass],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component for granting an item request
|
* Component for granting an item request
|
||||||
@@ -59,11 +68,39 @@ export class GrantRequestCopyComponent implements OnInit {
|
|||||||
message$: Observable<string>;
|
message$: Observable<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the item should be open access, to avoid future requests
|
* Whether the item should be open access, to avoid future requests
|
||||||
* Defaults to false
|
* Defaults to false
|
||||||
*/
|
*/
|
||||||
suggestOpenAccess = false;
|
suggestOpenAccess = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of integers determining valid access periods in seconds
|
||||||
|
*/
|
||||||
|
validAccessPeriods$: Observable<number[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected access period
|
||||||
|
*/
|
||||||
|
accessPeriod: any = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will this email attach file(s) directly, or send a secure link with an access token to provide temporary access?
|
||||||
|
* This will be false if the access token is populated, since the configuration and min file size checks are
|
||||||
|
* done at the time of request creation, with a default of true.
|
||||||
|
*/
|
||||||
|
sendAsAttachment = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview link to be sent to a request applicant
|
||||||
|
*/
|
||||||
|
previewLinkOptions: {
|
||||||
|
routerLink: string,
|
||||||
|
queryParams: any,
|
||||||
|
};
|
||||||
|
previewLink: string;
|
||||||
|
|
||||||
|
protected readonly hasValue = hasValue;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -71,17 +108,33 @@ export class GrantRequestCopyComponent implements OnInit {
|
|||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private itemRequestService: ItemRequestDataService,
|
private itemRequestService: ItemRequestDataService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
|
private hardRedirectService: HardRedirectService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Get item request data via the router (async)
|
||||||
this.itemRequestRD$ = this.route.data.pipe(
|
this.itemRequestRD$ = this.route.data.pipe(
|
||||||
map((data) => data.request as RemoteData<ItemRequest>),
|
map((data) => data.request as RemoteData<ItemRequest>),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
|
tap((rd) => {
|
||||||
|
// If an access token is present then the backend has checked configuration and file sizes
|
||||||
|
// and appropriately created a token to use with a secure link instead of attaching file directly
|
||||||
|
if (rd.hasSucceeded && hasValue(rd.payload.accessToken)) {
|
||||||
|
this.sendAsAttachment = false;
|
||||||
|
this.previewLinkOptions = getAccessTokenRequestRoute(rd.payload.itemId, rd.payload.accessToken);
|
||||||
|
this.previewLink = this.hardRedirectService.getCurrentOrigin()
|
||||||
|
+ this.previewLinkOptions.routerLink + '?accessToken=' + rd.payload.accessToken;
|
||||||
|
}
|
||||||
|
}),
|
||||||
redirectOn4xx(this.router, this.authService),
|
redirectOn4xx(this.router, this.authService),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get configured access periods
|
||||||
|
this.validAccessPeriods$ = this.itemRequestService.getConfiguredAccessPeriods();
|
||||||
|
|
||||||
|
// Get the subject line of the email
|
||||||
this.subject$ = this.translateService.get('grant-request-copy.email.subject');
|
this.subject$ = this.translateService.get('grant-request-copy.email.subject');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +145,7 @@ export class GrantRequestCopyComponent implements OnInit {
|
|||||||
grant(email: RequestCopyEmail) {
|
grant(email: RequestCopyEmail) {
|
||||||
this.itemRequestRD$.pipe(
|
this.itemRequestRD$.pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
switchMap((itemRequest: ItemRequest) => this.itemRequestService.grant(itemRequest.token, email, this.suggestOpenAccess)),
|
switchMap((itemRequest: ItemRequest) => this.itemRequestService.grant(itemRequest.token, email, this.suggestOpenAccess, this.accessPeriod)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
).subscribe((rd) => {
|
).subscribe((rd) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
@@ -104,4 +157,8 @@ export class GrantRequestCopyComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectAccessPeriod(accessPeriod: number) {
|
||||||
|
this.accessPeriod = accessPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user