83635: Grant/Deny item requests

This commit is contained in:
Kristof De Langhe
2021-10-05 14:55:34 +02:00
committed by Art Lowel
parent 11bf10cbde
commit 120b9f5ce9
15 changed files with 364 additions and 22 deletions

View File

@@ -183,6 +183,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
{
path: REQUEST_COPY_MODULE_PATH,
loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
},
{
path: FORBIDDEN_PATH,

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { filter, find, map } from 'rxjs/operators';
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 } from '../shared/operators';
import { getFirstCompletedRemoteData, sendRequest } from '../shared/operators';
import { RemoteData } from './remote-data';
import { PostRequest } from './request.models';
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';
@@ -14,11 +14,13 @@ 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 } from '@angular/common/http';
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 bitstreamformats endpoint
* A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint
*/
@Injectable(
{
@@ -46,12 +48,20 @@ export class ItemRequestDataService extends DataService<ItemRequest> {
return this.halService.getEndpoint(this.linkPath);
}
getFindItemRequestEndpoint(requestID: string): Observable<string> {
/**
* 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}/${requestID}`));
map((href: string) => `${href}/${token}`));
}
/**
* Request a copy of an item
* @param itemRequest
*/
requestACopy(itemRequest: ItemRequest): Observable<RemoteData<ItemRequest>> {
const requestId = this.requestService.generateRequestId();
@@ -70,4 +80,52 @@ export class ItemRequestDataService extends DataService<ItemRequest> {
);
}
/**
* 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);
}
}

View File

@@ -3,7 +3,7 @@
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
<p>{{'deny-request-copy.intro' | translate}}</p>
<ds-email-request-copy></ds-email-request-copy>
<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>

View File

@@ -1,27 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { map } from 'rxjs/operators';
import { map, switchMap } from 'rxjs/operators';
import { ItemRequest } from '../../core/shared/item-request.model';
import { Observable } from 'rxjs/internal/Observable';
import {
getFirstCompletedRemoteData,
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,
) {
}
@@ -32,6 +62,51 @@ export class DenyRequestCopyComponent implements OnInit {
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);
}
});
}
}

View File

@@ -8,17 +8,18 @@
</div>
<div class="form-group">
<label for="message">{{ 'grant-deny-request-copy.email.message' | translate }}</label>
<textarea class="form-control" id="message" rows="3" [ngClass]="{'is-invalid': !message || message.length === 0}" [(ngModel)]="message" name="message"></textarea>
<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">
<button (click)="submit()"
[disabled]="!message || message.length === 0 || !subject || subject.length === 0"
class="btn btn-primary mr-auto"
class="btn btn-success mr-auto"
title="{{'grant-deny-request-copy.email.send' | translate }}">
{{'grant-deny-request-copy.email.send' | translate }}
<i class="fa fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
</button>
<button (click)="return()"
class="btn btn-outline-secondary"

View File

@@ -7,19 +7,38 @@ import { Location } from '@angular/common';
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();
}

View File

@@ -1,3 +1,6 @@
/**
* A class representing an email to send back to the user requesting an item
*/
export class RequestCopyEmail {
constructor(public subject: string,
public message: string) {

View File

@@ -1,12 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../../core/core.reducers';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder } from '@angular/forms';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service';
import { map, switchMap } from 'rxjs/operators';
import { ItemRequest } from '../../core/shared/item-request.model';
import { Observable } from 'rxjs/internal/Observable';
@@ -27,12 +20,33 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
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 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(

View File

@@ -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>

View File

@@ -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);
}
});
}
}

View File

@@ -4,6 +4,7 @@ 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: [
@@ -21,7 +22,11 @@ import { DenyRequestCopyComponent } from './deny-request-copy/deny-request-copy.
{
path: REQUEST_COPY_DENY_PATH,
component: DenyRequestCopyComponent,
}
},
{
path: REQUEST_COPY_GRANT_PATH,
component: GrantRequestCopyComponent,
},
]
}
])

View File

@@ -5,6 +5,7 @@ import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-d
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: [
@@ -16,12 +17,13 @@ import { EmailRequestCopyComponent } from './email-request-copy/email-request-co
GrantDenyRequestCopyComponent,
DenyRequestCopyComponent,
EmailRequestCopyComponent,
GrantRequestCopyComponent,
],
providers: []
})
/**
* Module related to components used to register a new user
* Module related to components used to grant or deny an item request
*/
export class RequestCopyModule {

View File

@@ -6,6 +6,9 @@ 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>> {

View File

@@ -1206,10 +1206,18 @@
"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 author (or co-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",
@@ -1445,6 +1453,10 @@
"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 there is no reason to keep it restricted, 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",
@@ -1461,6 +1473,20 @@
"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 author (or co-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",