Request-a-copy: Changes to support access expiry as delta/date storage - psql max

This commit is contained in:
Kim Shepherd
2025-03-24 12:56:32 +01:00
parent 57b618ce34
commit 1fff3b5b86
7 changed files with 58 additions and 39 deletions

View File

@@ -125,7 +125,7 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
* @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) * @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, accessPeriod = 0): Observable<RemoteData<ItemRequest>> { grant(token: string, email: RequestCopyEmail, suggestOpenAccess = false, accessPeriod: string = null): Observable<RemoteData<ItemRequest>> {
return this.process(token, email, true, suggestOpenAccess, accessPeriod); return this.process(token, email, true, suggestOpenAccess, accessPeriod);
} }
@@ -137,7 +137,7 @@ export class ItemRequestDataService extends IdentifiableDataService<ItemRequest>
* @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) * @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, accessPeriod = 0): Observable<RemoteData<ItemRequest>> { process(token: string, email: RequestCopyEmail, grant: boolean, suggestOpenAccess = false, accessPeriod: string = null): Observable<RemoteData<ItemRequest>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
this.getItemRequestEndpointByToken(token).pipe( this.getItemRequestEndpointByToken(token).pipe(
@@ -161,26 +161,6 @@ 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. * Get a sanitized item request using the searchBy method and the access token sent to the original requester.
* *

View File

@@ -15,7 +15,7 @@
</div> </div>
<!-- Display access periods if more than one was bound to input. The parent component (grant-request-copy) <!-- 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. --> sends an empty list if the feature is not enabled or applicable to this request. -->
@if (hasValue(validAccessPeriods) && validAccessPeriods.length > 0) { @if (hasValue(validAccessPeriods$ | async) && (validAccessPeriods$ | async).length > 0) {
<div class="form-group"> <div class="form-group">
<label for="accessPeriod">{{ 'grant-request-copy.access-period.header' | translate }}</label> <label for="accessPeriod">{{ 'grant-request-copy.access-period.header' | translate }}</label>
<div ngbDropdown class="d-block"> <div ngbDropdown class="d-block">
@@ -28,7 +28,7 @@
</button> </button>
<!-- Access period dropdown --> <!-- Access period dropdown -->
<div ngbDropdownMenu aria-labelledby="accessPeriod"> <div ngbDropdownMenu aria-labelledby="accessPeriod">
@for (accessPeriod of validAccessPeriods; track accessPeriod) { @for (accessPeriod of (validAccessPeriods$ | async); track accessPeriod) {
<button <button
ngbDropdownItem ngbDropdownItem
class="list-element" class="list-element"

View File

@@ -1,6 +1,7 @@
import 'altcha'; import 'altcha';
import { import {
AsyncPipe,
Location, Location,
NgClass, NgClass,
} from '@angular/common'; } from '@angular/common';
@@ -8,12 +9,18 @@ import {
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
OnDestroy,
OnInit, 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 { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import {
Observable,
Subject,
} from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive'; import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
@@ -24,16 +31,20 @@ import { RequestCopyEmail } from './request-copy-email.model';
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, NgbDropdownModule], imports: [FormsModule, NgClass, TranslateModule, BtnDisabledDirective, NgbDropdownModule, AsyncPipe],
}) })
/** /**
* 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 implements OnInit { export class EmailRequestCopyComponent implements OnInit, OnDestroy {
/** /**
* 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>();
/**
* Selected access period emmitter, sending the new period up to the parent component
*/
@Output() selectedAccessPeriod: EventEmitter<string> = new EventEmitter(); @Output() selectedAccessPeriod: EventEmitter<string> = new EventEmitter();
/** /**
@@ -49,7 +60,7 @@ export class EmailRequestCopyComponent implements OnInit {
/** /**
* A list of valid access periods to render in a drop-down menu * A list of valid access periods to render in a drop-down menu
*/ */
@Input() validAccessPeriods: string [] = []; @Input() validAccessPeriods$: Observable<string[]>;
/** /**
* The selected access period, e.g. +7DAYS, +12MONTHS, FOREVER. These will be * The selected access period, e.g. +7DAYS, +12MONTHS, FOREVER. These will be
@@ -57,16 +68,37 @@ export class EmailRequestCopyComponent implements OnInit {
*/ */
accessPeriod = 'FOREVER'; accessPeriod = 'FOREVER';
/**
* Destroy subject for unsubscribing from observables
* @private
*/
private destroy$ = new Subject<void>();
protected readonly hasValue = hasValue; protected readonly hasValue = hasValue;
constructor(protected location: Location) { constructor(protected location: Location) {
} }
/**
* Initialise subscription to async valid access periods (from configuration service)
*/
ngOnInit(): void { ngOnInit(): void {
// If access periods are present, set the default to the first in the array this.validAccessPeriods$.pipe(
if (hasValue(this.validAccessPeriods) && this.validAccessPeriods.length > 0) { takeUntil(this.destroy$),
this.selectAccessPeriod(this.validAccessPeriods[0]); ).subscribe((validAccessPeriods) => {
if (hasValue(validAccessPeriods) && validAccessPeriods.length > 0) {
this.selectAccessPeriod(validAccessPeriods[0]);
} }
});
}
/**
* Clean up subscriptions and selectors
*/
ngOnDestroy(): void {
this.selectedAccessPeriod.complete();
this.destroy$.next();
this.destroy$.complete();
} }
/** /**

View File

@@ -4,6 +4,7 @@ import {
Input, Input,
Output, Output,
} from '@angular/core'; } from '@angular/core';
import { Observable } from 'rxjs';
import { ThemedComponent } from 'src/app/shared/theme-support/themed.component'; import { ThemedComponent } from 'src/app/shared/theme-support/themed.component';
import { EmailRequestCopyComponent } from './email-request-copy.component'; import { EmailRequestCopyComponent } from './email-request-copy.component';
@@ -28,7 +29,7 @@ export class ThemedEmailRequestCopyComponent extends ThemedComponent<EmailReques
/** /**
* Event emitter for a selected / changed access period * Event emitter for a selected / changed access period
*/ */
@Output() selectedAccessPeriod: EventEmitter<number> = new EventEmitter(); @Output() selectedAccessPeriod: EventEmitter<string> = new EventEmitter();
/** /**
* The subject of the email * The subject of the email
@@ -43,10 +44,10 @@ export class ThemedEmailRequestCopyComponent extends ThemedComponent<EmailReques
/** /**
* A list of valid access periods, if configured * A list of valid access periods, if configured
*/ */
@Input() validAccessPeriods: number[]; @Input() validAccessPeriods$: Observable<string[]>;
protected inAndOutputNames: (keyof EmailRequestCopyComponent & keyof this)[] = ['send', 'subject', 'message', 'validAccessPeriods', 'selectedAccessPeriod']; protected inAndOutputNames: (keyof EmailRequestCopyComponent & keyof this)[] = ['send', 'subject', 'message', 'selectedAccessPeriod', 'validAccessPeriods$'];
protected getComponentName(): string { protected getComponentName(): string {
return 'EmailRequestCopyComponent'; return 'EmailRequestCopyComponent';

View File

@@ -21,9 +21,9 @@
<!-- Only send access periods for display if an access token was present --> <!-- Only send access periods for display if an access token was present -->
<ds-email-request-copy [subject]="subject$ | async" <ds-email-request-copy [subject]="subject$ | async"
[message]="message$ | async" [message]="message$ | async"
[validAccessPeriods]="(hasValue(itemRequestRD.payload.accessToken) ? (validAccessPeriods$ | async) : [])"
(send)="grant($event)" (send)="grant($event)"
(selectedAccessPeriod)="selectAccessPeriod($event)" (selectedAccessPeriod)="selectAccessPeriod($event)"
[validAccessPeriods$]="validAccessPeriods$"
> >
<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">

View File

@@ -81,7 +81,7 @@ export class GrantRequestCopyComponent implements OnInit {
/** /**
* The currently selected access period * The currently selected access period
*/ */
accessPeriod: any = 0; accessPeriod: string = null;
/** /**
* Will this email attach file(s) directly, or send a secure link with an access token to provide temporary access? * Will this email attach file(s) directly, or send a secure link with an access token to provide temporary access?
@@ -113,6 +113,9 @@ export class GrantRequestCopyComponent implements OnInit {
} }
/**
* Initialize the component - get the item request from route data an duse it to populate the form
*/
ngOnInit(): void { ngOnInit(): void {
// Get item request data via the router (async) // Get item request data via the router (async)
this.itemRequestRD$ = this.route.data.pipe( this.itemRequestRD$ = this.route.data.pipe(
@@ -157,7 +160,7 @@ export class GrantRequestCopyComponent implements OnInit {
}); });
} }
selectAccessPeriod(accessPeriod: number) { selectAccessPeriod(accessPeriod: string) {
this.accessPeriod = accessPeriod; this.accessPeriod = accessPeriod;
} }

View File

@@ -1,4 +1,7 @@
import { NgClass } from '@angular/common'; import {
AsyncPipe,
NgClass,
} from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -13,7 +16,7 @@ import { BtnDisabledDirective } from '../../../../../app/shared/btn-disabled.dir
// templateUrl: './email-request-copy.component.html', // templateUrl: './email-request-copy.component.html',
templateUrl: './../../../../../app/request-copy/email-request-copy/email-request-copy.component.html', templateUrl: './../../../../../app/request-copy/email-request-copy/email-request-copy.component.html',
standalone: true, standalone: true,
imports: [FormsModule, NgClass, TranslateModule, BtnDisabledDirective], imports: [FormsModule, NgClass, TranslateModule, BtnDisabledDirective, AsyncPipe],
}) })
export class EmailRequestCopyComponent export class EmailRequestCopyComponent
extends BaseComponent { extends BaseComponent {