From 506883c96060c5cd9ff6f3199fca2bcc26d4ea78 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 20 Sep 2021 16:26:33 +0200 Subject: [PATCH 01/12] 83635: Request a copy - Request page --- src/app/app-routing-paths.ts | 3 + .../bitstream-page-routing.module.ts | 9 + .../data/feature-authorization/feature-id.ts | 1 + .../core/data/item-request-data.service.ts | 59 ++++ src/app/core/shared/item-request.model.ts | 79 ++++++ .../core/shared/item-request.resource-type.ts | 9 + ...tstream-request-a-copy-page.component.html | 86 ++++++ ...ream-request-a-copy-page.component.spec.ts | 256 ++++++++++++++++++ ...bitstream-request-a-copy-page.component.ts | 199 ++++++++++++++ .../file-download-link.component.html | 3 +- .../file-download-link.component.spec.ts | 128 +++++++-- .../file-download-link.component.ts | 36 ++- src/app/shared/shared.module.ts | 3 + .../file/section-upload-file.component.html | 2 +- src/assets/i18n/en.json5 | 34 +++ 15 files changed, 871 insertions(+), 36 deletions(-) create mode 100644 src/app/core/data/item-request-data.service.ts create mode 100644 src/app/core/shared/item-request.model.ts create mode 100644 src/app/core/shared/item-request.resource-type.ts create mode 100644 src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html create mode 100644 src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts create mode 100644 src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 81b0755d11..f5a4414756 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -22,6 +22,9 @@ export function getBitstreamModuleRoute() { export function getBitstreamDownloadRoute(bitstream): string { return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(); } +export function getBitstreamRequestACopyRoute(bitstream): string { + return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'request-a-copy').toString(); +} export const ADMIN_MODULE_PATH = 'admin'; diff --git a/src/app/bitstream-page/bitstream-page-routing.module.ts b/src/app/bitstream-page/bitstream-page-routing.module.ts index 27b9db9a05..1027c85b46 100644 --- a/src/app/bitstream-page/bitstream-page-routing.module.ts +++ b/src/app/bitstream-page/bitstream-page-routing.module.ts @@ -10,6 +10,7 @@ import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/re import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; +import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; const EDIT_BITSTREAM_PATH = ':id/edit'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; @@ -44,6 +45,14 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; bitstream: BitstreamPageResolver }, }, + { + // Resolve angular bitstream download URLs + path: ':id/request-a-copy', + component: BitstreamRequestACopyPageComponent, + resolve: { + bitstream: BitstreamPageResolver + }, + }, { path: EDIT_BITSTREAM_PATH, component: EditBitstreamPageComponent, diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index ac045b93b0..64a4fdc60f 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -14,6 +14,7 @@ export enum FeatureID { IsCollectionAdmin = 'isCollectionAdmin', IsCommunityAdmin = 'isCommunityAdmin', CanDownload = 'canDownload', + CanRequestACopy = 'canRequestACopy', CanManageVersions = 'canManageVersions', CanManageBitstreamBundles = 'canManageBitstreamBundles', CanManageRelationships = 'canManageRelationships', diff --git a/src/app/core/data/item-request-data.service.ts b/src/app/core/data/item-request-data.service.ts new file mode 100644 index 0000000000..dc11e2b0c5 --- /dev/null +++ b/src/app/core/data/item-request-data.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { 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 { RemoteData } from './remote-data'; +import { PostRequest } from './request.models'; +import { RequestService } from './request.service'; +import { ItemRequest } from '../shared/item-request.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; + +/** + * A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint + */ +@Injectable( + { + providedIn: 'root', + } +) +export class ItemRequestDataService { + + protected linkPath = 'itemrequests'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected halService: HALEndpointService) { + } + + getItemRequestEndpoint(): Observable { + return this.halService.getEndpoint(this.linkPath); + } + + getFindItemRequestEndpoint(requestID: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + map((href: string) => `${href}/${requestID}`)); + } + + requestACopy(itemRequest: ItemRequest): Observable> { + const requestId = this.requestService.generateRequestId(); + + const href$ = this.getItemRequestEndpoint(); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PostRequest(requestId, href, itemRequest); + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId).pipe( + getFirstCompletedRemoteData() + ); + } + +} diff --git a/src/app/core/shared/item-request.model.ts b/src/app/core/shared/item-request.model.ts new file mode 100644 index 0000000000..b14ed07656 --- /dev/null +++ b/src/app/core/shared/item-request.model.ts @@ -0,0 +1,79 @@ +import { autoserialize } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { ResourceType } from './resource-type'; +import { ITEM_REQUEST } from './item-request.resource-type'; + +/** + * Model class for a Configuration Property + */ +@typedObject +export class ItemRequest { + static type = ITEM_REQUEST; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * opaque string which uniquely identifies this request + */ + @autoserialize + token: string; + + /** + * true if the request is for all bitstreams of the item. + */ + @autoserialize + allfiles: boolean; + /** + * email address of the person requesting the files. + */ + @autoserialize + requestEmail: string; + /** + * Human-readable name of the person requesting the files. + */ + @autoserialize + requestName: string; + /** + * arbitrary message provided by the person requesting the files. + */ + @autoserialize + requestMessage: string; + /** + * date that the request was recorded. + */ + @autoserialize + requestDate: string; + /** + * true if the request has been granted. + */ + @autoserialize + acceptRequest: boolean; + /** + * date that the request was granted or denied. + */ + @autoserialize + decisionDate: string; + /** + * date on which the request is considered expired. + */ + @autoserialize + expires: string; + /** + * UUID of the requested Item. + */ + @autoserialize + itemId: string; + /** + * UUID of the requested bitstream. + */ + @autoserialize + bitstreamId: string; + + +} diff --git a/src/app/core/shared/item-request.resource-type.ts b/src/app/core/shared/item-request.resource-type.ts new file mode 100644 index 0000000000..0535ef1948 --- /dev/null +++ b/src/app/core/shared/item-request.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ItemRequest. + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const ITEM_REQUEST = new ResourceType('itemrequest'); diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html new file mode 100644 index 0000000000..4ab6963f74 --- /dev/null +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html @@ -0,0 +1,86 @@ +
+

{{'bitstream-request-a-copy.header' | translate}}

+
+ {{'bitstream-request-a-copy.alert.canDownload1' | translate}} + {{'bitstream-request-a-copy.alert.canDownload2'| translate}} +
+
+

{{'bitstream-request-a-copy.intro' | translate}}

+

{{itemName}}

+
+
+ +
+
+
+ + +
+ + {{ 'bitstream-request-a-copy.name.error' | translate }} + +
+
+
+
+
+ + +
+ + {{ 'bitstream-request-a-copy.email.error' | translate }} + +
+ {{'bitstream-request-a-copy.email.hint' |translate}} +
+
+
+
+
{{'bitstream-request-a-copy.allfiles.label' |translate}}
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+ + + {{'bitstream-request-a-copy.return' | translate}} + + + +
+
+
diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts new file mode 100644 index 0000000000..20df5dfb03 --- /dev/null +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts @@ -0,0 +1,256 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { AuthService } from '../../core/auth/auth.service'; +import { of as observableOf } from 'rxjs'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../remote-data.utils'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page.component'; +import { By } from '@angular/platform-browser'; +import { RouterStub } from '../testing/router.stub'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NotificationsServiceStub } from '../testing/notifications-service.stub'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../mocks/dso-name.service.mock'; +import { Item } from '../../core/shared/item.model'; +import { Bundle } from '../../core/shared/bundle.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { Location } from '@angular/common'; + + +describe('BitstreamRequestACopyPageComponent', () => { + let component: BitstreamRequestACopyPageComponent; + let fixture: ComponentFixture; + + let authService: AuthService; + let authorizationService: AuthorizationDataService; + let activatedRoute; + let router; + let itemRequestDataService; + let notificationsService; + let location; + + let item: Item; + let bitstream: Bitstream; + let eperson; + + function init() { + eperson = Object.assign(new EPerson(), { + email: 'test@mail.org', + metadata: { + 'eperson.firstname': [{value: 'Test'}], + 'eperson.lastname': [{value: 'User'}], + } + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(false), + getAuthenticatedUserFromStore: observableOf(eperson) + }); + authorizationService = jasmine.createSpyObj('authorizationSerivice', { + isAuthorized: observableOf(true) + }); + + itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', { + requestACopy: createSuccessfulRemoteDataObject$({}) + }); + + location = jasmine.createSpyObj('location', { + back: {} + }); + + notificationsService = new NotificationsServiceStub(); + + item = Object.assign(new Item(), {uuid: 'item-uuid'}); + + const bundle = Object.assign(new Bundle(), { + uuid: 'bundle-uuid', + item: createSuccessfulRemoteDataObject$(item) + }); + + bitstream = Object.assign(new Bitstream(), { + uuid: 'bitstreamUuid', + bundle: createSuccessfulRemoteDataObject$(bundle), + _links: { + content: {href: 'bitstream-content-link'}, + self: {href: 'bitstream-self-link'}, + } + }); + + activatedRoute = { + data: observableOf({ + bitstream: createSuccessfulRemoteDataObject( + bitstream + ) + }) + }; + + router = new RouterStub(); + } + + function initTestbed() { + TestBed.configureTestingModule({ + imports: [CommonModule, TranslateModule.forRoot(), FormsModule, ReactiveFormsModule], + declarations: [BitstreamRequestACopyPageComponent], + providers: [ + {provide: Location, useValue: location}, + {provide: ActivatedRoute, useValue: activatedRoute}, + {provide: Router, useValue: router}, + {provide: AuthorizationDataService, useValue: authorizationService}, + {provide: AuthService, useValue: authService}, + {provide: ItemRequestDataService, useValue: itemRequestDataService}, + {provide: NotificationsService, useValue: notificationsService}, + {provide: DSONameService, useValue: new DSONameServiceMock()}, + ] + }) + .compileComponents(); + } + + describe('init', () => { + beforeEach(waitForAsync(() => { + init(); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should init the comp', () => { + expect(component).toBeTruthy(); + }); + }); + + describe('should show a form to request a copy', () => { + describe('when the user is not logged in', () => { + beforeEach(waitForAsync(() => { + init(); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('show the form with no values filled in based on the user', () => { + expect(component.name.value).toEqual(''); + expect(component.email.value).toEqual(''); + expect(component.allfiles.value).toEqual('true'); + expect(component.message.value).toEqual(''); + }); + }); + + describe('when the user is logged in', () => { + beforeEach(waitForAsync(() => { + init(); + (authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true)); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('show the form with values filled in based on the user', () => { + fixture.detectChanges(); + expect(component.name.value).toEqual(eperson.name); + expect(component.email.value).toEqual(eperson.email); + expect(component.allfiles.value).toEqual('true'); + expect(component.message.value).toEqual(''); + }); + }); + describe('when the user has authorization to download the file', () => { + beforeEach(waitForAsync(() => { + init(); + (authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true)); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should show an alert indicating the user can download the file', () => { + const alert = fixture.debugElement.query(By.css('.alert')).nativeElement; + expect(alert.innerHTML).toContain('bitstream-request-a-copy.alert.canDownload'); + }); + }); + }); + + describe('onSubmit', () => { + describe('onSuccess', () => { + beforeEach(waitForAsync(() => { + init(); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should take the current form information and submit it', () => { + component.name.patchValue('User Name'); + component.email.patchValue('user@name.org'); + component.allfiles.patchValue('false'); + component.message.patchValue('I would like to request a copy'); + + component.onSubmit(); + const itemRequest = Object.assign(new ItemRequest(), + { + itemId: item.uuid, + bitstreamId: bitstream.uuid, + allfiles: 'false', + requestEmail: 'user@name.org', + requestName: 'User Name', + requestMessage: 'I would like to request a copy' + }); + + expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest); + expect(notificationsService.success).toHaveBeenCalled(); + expect(location.back).toHaveBeenCalled(); + }); + }); + + describe('onFail', () => { + beforeEach(waitForAsync(() => { + init(); + (itemRequestDataService.requestACopy as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should take the current form information and submit it', () => { + component.name.patchValue('User Name'); + component.email.patchValue('user@name.org'); + component.allfiles.patchValue('false'); + component.message.patchValue('I would like to request a copy'); + + component.onSubmit(); + const itemRequest = Object.assign(new ItemRequest(), + { + itemId: item.uuid, + bitstreamId: bitstream.uuid, + allfiles: 'false', + requestEmail: 'user@name.org', + requestName: 'User Name', + requestMessage: 'I would like to request a copy' + }); + + expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest); + expect(notificationsService.error).toHaveBeenCalled(); + expect(location.back).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts new file mode 100644 index 0000000000..35b56e5434 --- /dev/null +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts @@ -0,0 +1,199 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { map, switchMap, take } from 'rxjs/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { hasValue, isNotEmpty } from '../empty.util'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { getBitstreamDownloadRoute, getForbiddenRoute } from '../../app-routing-paths'; +import { TranslateService } from '@ngx-translate/core'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { Item } from '../../core/shared/item.model'; +import { NotificationsService } from '../notifications/notifications.service'; +import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { Location } from '@angular/common'; + +@Component({ + selector: 'ds-bitstream-request-a-copy-page', + templateUrl: './bitstream-request-a-copy-page.component.html' +}) +/** + * Page component for requesting a copy for a bitstream + */ +export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { + + bitstream$: Observable; + + canDownload$: Observable; + private subs: Subscription[] = []; + requestCopyForm: FormGroup; + bitstream: Bitstream; + item: Item; + itemName: string; + + constructor(private location: Location, + private translateService: TranslateService, + private route: ActivatedRoute, + protected router: Router, + private authorizationService: AuthorizationDataService, + private auth: AuthService, + private formBuilder: FormBuilder, + private itemRequestDataService: ItemRequestDataService, + private notificationsService: NotificationsService, + private dsoNameService: DSONameService, + ) { + } + + ngOnInit(): void { + this.requestCopyForm = this.formBuilder.group({ + name: new FormControl('', { + validators: [Validators.required], + }), + email: new FormControl('', { + validators: [Validators.required, + Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$')] + }), + allfiles: new FormControl(''), + message: new FormControl(''), + }); + + + this.bitstream$ = this.route.data.pipe( + map((data) => data.bitstream), + getFirstSucceededRemoteDataPayload() + ); + + this.subs.push(this.bitstream$.subscribe((bitstream) => { + this.bitstream = bitstream; + })); + + this.subs.push(this.bitstream$.pipe( + switchMap((bitstream) => bitstream.bundle), + getFirstSucceededRemoteDataPayload(), + switchMap((bundle) => bundle.item), + getFirstSucceededRemoteDataPayload(), + ).subscribe((item) => { + this.item = item; + this.itemName = this.dsoNameService.getName(item); + })); + + this.canDownload$ = this.bitstream$.pipe( + switchMap((bitstream) => this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(bitstream) ? bitstream.self : undefined)) + ); + const canRequestCopy$ = this.bitstream$.pipe( + switchMap((bitstream) => this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(bitstream) ? bitstream.self : undefined)), + ); + + this.subs.push(observableCombineLatest([this.canDownload$, canRequestCopy$]).subscribe(([canDownload, canRequestCopy]) => { + if (!canDownload && !canRequestCopy) { + this.router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true}); + } + })); + this.initValues(); + } + + get name() { + return this.requestCopyForm.get('name'); + } + + get email() { + return this.requestCopyForm.get('email'); + } + + get message() { + return this.requestCopyForm.get('message'); + } + + get allfiles() { + return this.requestCopyForm.get('allfiles'); + } + + /** + * Initialise the form values based on the current user. + */ + private initValues() { + this.getCurrentUser().pipe(take(1)).subscribe((user) => { + this.requestCopyForm.patchValue({allfiles: 'true'}); + if (hasValue(user)) { + this.requestCopyForm.patchValue({name: user.name, email: user.email}); + console.log('ping'); + } + }); + } + + /** + * Retrieve the current user + */ + private getCurrentUser(): Observable { + return this.auth.isAuthenticated().pipe( + switchMap((authenticated) => { + if (authenticated) { + return this.auth.getAuthenticatedUserFromStore(); + } else { + return observableOf(undefined); + } + }) + ); + + } + + /** + * Submit the the form values as an item request to the server. + * When the submission is successful, the user will be redirected to the item page and a success notification will be shown. + * When the submission fails, the user will stay on the page and an error notification will be shown + */ + onSubmit() { + const itemRequest = new ItemRequest(); + if (hasValue(this.item)) { + itemRequest.itemId = this.item.uuid; + } + itemRequest.bitstreamId = this.bitstream.uuid; + itemRequest.allfiles = this.allfiles.value; + itemRequest.requestEmail = this.email.value; + itemRequest.requestName = this.name.value; + itemRequest.requestMessage = this.message.value; + + this.itemRequestDataService.requestACopy(itemRequest).pipe( + getFirstCompletedRemoteData() + ).subscribe((rd) => { + if (rd.hasSucceeded) { + this.notificationsService.success(this.translateService.get('bitstream-request-a-copy.submit.success')); + this.navigateBack(); + } else { + this.notificationsService.error(this.translateService.get('bitstream-request-a-copy.submit.error')); + } + }); + } + + ngOnDestroy(): void { + if (hasValue(this.subs)) { + this.subs.forEach((sub) => { + if (hasValue(sub)) { + sub.unsubscribe(); + } + }); + } + } + + /** + * Navigates back to the user's previous location + */ + navigateBack() { + this.location.back(); + } + + /** + * Retrieves the link to the bistream download page + */ + getBitstreamLink() { + return [getBitstreamDownloadRoute(this.bitstream)]; + } +} diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index 497502d586..fc4b3b97ac 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,4 +1,5 @@ - + + diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index 6f7f50e585..80a30e3bdd 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -1,62 +1,132 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { FileDownloadLinkComponent } from './file-download-link.component'; -import { AuthService } from '../../core/auth/auth.service'; -import { FileService } from '../../core/shared/file.service'; -import { of as observableOf } from 'rxjs'; import { Bitstream } from '../../core/shared/bitstream.model'; import { By } from '@angular/platform-browser'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { getBitstreamModuleRoute } from '../../app-routing-paths'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; describe('FileDownloadLinkComponent', () => { let component: FileDownloadLinkComponent; let fixture: ComponentFixture; - let authService: AuthService; - let fileService: FileService; + let scheduler; + let authorizationService: AuthorizationDataService; + let bitstream: Bitstream; function init() { - authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: cold('-a', {a: true}) }); - fileService = jasmine.createSpyObj('fileService', ['downloadFile']); bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', + _links: { + self: {href: 'obj-selflink'} + } }); } - beforeEach(waitForAsync(() => { - init(); + function initTestbed() { TestBed.configureTestingModule({ declarations: [FileDownloadLinkComponent], providers: [ - { provide: AuthService, useValue: authService }, - { provide: FileService, useValue: fileService }, + {provide: AuthorizationDataService, useValue: authorizationService}, ] }) .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(FileDownloadLinkComponent); - component = fixture.componentInstance; - component.bitstream = bitstream; - fixture.detectChanges(); - }); + } describe('init', () => { - describe('getBitstreamPath', () => { - it('should set the bitstreamPath based on the input bitstream', () => { - expect(component.bitstreamPath).toEqual(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); + describe('when the user has download rights', () => { + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + init(); + initTestbed(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FileDownloadLinkComponent); + component = fixture.componentInstance; + component.bitstream = bitstream; + fixture.detectChanges(); + }); + it('should return the bitstreamPath based on the input bitstream', () => { + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()})); + expect(component.canDownload$).toBeObservable(cold('--a', {a: true})); + + }); + it('should init the component', () => { + scheduler.flush(); + fixture.detectChanges(); + const link = fixture.debugElement.query(By.css('a')).nativeElement; + expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); + const lock = fixture.debugElement.query(By.css('.fa-lock')); + expect(lock).toBeNull(); + }); + }); + describe('when the user has no download rights but has the right to request a copy', () => { + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + init(); + (authorizationService.isAuthorized as jasmine.Spy).and.callFake((featureId, object) => { + if (featureId === FeatureID.CanDownload) { + return cold('-a', {a: false}); + } + return cold('-a', {a: true}); + }); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(FileDownloadLinkComponent); + component = fixture.componentInstance; + component.bitstream = bitstream; + fixture.detectChanges(); + }); + it('should return the bitstreamPath based on the input bitstream', () => { + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'request-a-copy').toString()})); + expect(component.canDownload$).toBeObservable(cold('--a', {a: false})); + + }); + it('should init the component', () => { + scheduler.flush(); + fixture.detectChanges(); + const link = fixture.debugElement.query(By.css('a')).nativeElement; + expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'request-a-copy').toString()); + const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; + expect(lock).toBeTruthy(); + }); + }); + describe('when the user has no download rights and no request a copy rights', () => { + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + init(); + (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(cold('-a', {a: false})); + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(FileDownloadLinkComponent); + component = fixture.componentInstance; + component.bitstream = bitstream; + fixture.detectChanges(); + }); + it('should return the bitstreamPath based on the input bitstream', () => { + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()})); + expect(component.canDownload$).toBeObservable(cold('--a', {a: false})); + + }); + it('should init the component', () => { + scheduler.flush(); + fixture.detectChanges(); + const link = fixture.debugElement.query(By.css('a')).nativeElement; + expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); + const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; + expect(lock).toBeTruthy(); + }); }); }); - - it('should init the component', () => { - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); - }); - }); }); diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index b415e1e701..2d98c97821 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -1,6 +1,11 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; -import { getBitstreamDownloadRoute } from '../../app-routing-paths'; +import { getBitstreamDownloadRoute, getBitstreamRequestACopyRoute } from '../../app-routing-paths'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { isNotEmpty } from '../empty.util'; +import { map } from 'rxjs/operators'; +import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs'; @Component({ selector: 'ds-file-download-link', @@ -29,13 +34,34 @@ export class FileDownloadLinkComponent implements OnInit { */ @Input() isBlank = false; - bitstreamPath: string; + @Input() enableRequestACopy = true; - ngOnInit() { - this.bitstreamPath = this.getBitstreamPath(); + bitstreamPath$: Observable; + + canDownload$: Observable; + + constructor( + private authorizationService: AuthorizationDataService, + ) { } - getBitstreamPath() { + ngOnInit() { + if (this.enableRequestACopy) { + this.canDownload$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined); + const canRequestACopy$ = this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined); + this.bitstreamPath$ = observableCombineLatest([this.canDownload$, canRequestACopy$]).pipe( + map(([canDownload, canRequestACopy]) => this.getBitstreamPath(canDownload, canRequestACopy)) + ); + } else { + this.bitstreamPath$ = observableOf(getBitstreamDownloadRoute(this.bitstream)); + this.canDownload$ = observableOf(true); + } + } + + getBitstreamPath(canDownload: boolean, canRequestACopy: boolean) { + if (!canDownload && canRequestACopy) { + return getBitstreamRequestACopyRoute(this.bitstream); + } return getBitstreamDownloadRoute(this.bitstream); } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9b993e551f..2488ecf1ed 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -233,6 +233,7 @@ import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.com import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component'; import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component'; import { SearchNavbarComponent } from '../search-navbar/search-navbar.component'; +import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; /** * Declaration needed to make sure all decorator functions are called in time @@ -431,6 +432,7 @@ const COMPONENTS = [ GroupSearchBoxComponent, FileDownloadLinkComponent, BitstreamDownloadPageComponent, + BitstreamRequestACopyPageComponent, CollectionDropdownComponent, ExportMetadataSelectorComponent, ConfirmationModalComponent, @@ -512,6 +514,7 @@ const ENTRY_COMPONENTS = [ CollectionDropdownComponent, FileDownloadLinkComponent, BitstreamDownloadPageComponent, + BitstreamRequestACopyPageComponent, CurationFormComponent, ExportMetadataSelectorComponent, ConfirmationModalComponent, diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.html b/src/app/submission/sections/upload/file/section-upload-file.component.html index 09c062d191..132f95dcb4 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.html +++ b/src/app/submission/sections/upload/file/section-upload-file.component.html @@ -10,7 +10,7 @@
- +
- + {{"item.page.filesection.download" | translate}}
@@ -74,7 +74,7 @@
- + {{"item.page.filesection.download" | translate}}
diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index f2d0a23935..692783e6ab 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -12,6 +12,8 @@ import { MenuItemType } from '../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { ThemedItemPageComponent } from './simple/themed-item-page.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; +import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; +import { BitstreamPageResolver } from '../bitstream-page/bitstream-page.resolver'; @NgModule({ imports: [ @@ -42,6 +44,10 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon path: UPLOAD_BITSTREAM_PATH, component: UploadBitstreamComponent, canActivate: [AuthenticatedGuard] + }, + { + path: ':request-a-copy', + component: BitstreamRequestACopyPageComponent, } ], data: { diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.html b/src/app/item-page/simple/field-components/file-section/file-section.component.html index 0fa5daa012..3d093f83c9 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.html @@ -1,7 +1,7 @@
- + {{file?.name}} ({{(file?.sizeBytes) | dsFileSize }}) diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html index 4ab6963f74..1fae737fdb 100644 --- a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.html @@ -5,8 +5,9 @@ {{'bitstream-request-a-copy.alert.canDownload2'| translate}}
-

{{'bitstream-request-a-copy.intro' | translate}}

-

{{itemName}}

+

{{'bitstream-request-a-copy.intro' | translate}} {{itemName}}

+

{{'bitstream-request-a-copy.intro.bitstream.one' | translate}} {{bitstreamName}}

+

{{'bitstream-request-a-copy.intro.bitstream.all' | translate}}

@@ -45,13 +46,13 @@
{{'bitstream-request-a-copy.allfiles.label' |translate}}
+ id="allfiles-true" formControlName="allfiles" value="true">
+ id="allfiles-false" formControlName="allfiles" value="false" [attr.disabled]="bitstream === undefined ? true : null ">
diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts index 20df5dfb03..cc44ef8587 100644 --- a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.spec.ts @@ -21,10 +21,10 @@ import { NotificationsService } from '../notifications/notifications.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../mocks/dso-name.service.mock'; import { Item } from '../../core/shared/item.model'; -import { Bundle } from '../../core/shared/bundle.model'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { ItemRequest } from '../../core/shared/item-request.model'; import { Location } from '@angular/common'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; describe('BitstreamRequestACopyPageComponent', () => { @@ -38,6 +38,7 @@ describe('BitstreamRequestACopyPageComponent', () => { let itemRequestDataService; let notificationsService; let location; + let bitstreamDataService; let item: Item; let bitstream: Bitstream; @@ -71,14 +72,8 @@ describe('BitstreamRequestACopyPageComponent', () => { item = Object.assign(new Item(), {uuid: 'item-uuid'}); - const bundle = Object.assign(new Bundle(), { - uuid: 'bundle-uuid', - item: createSuccessfulRemoteDataObject$(item) - }); - bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', - bundle: createSuccessfulRemoteDataObject$(bundle), _links: { content: {href: 'bitstream-content-link'}, self: {href: 'bitstream-self-link'}, @@ -87,12 +82,19 @@ describe('BitstreamRequestACopyPageComponent', () => { activatedRoute = { data: observableOf({ - bitstream: createSuccessfulRemoteDataObject( - bitstream + dso: createSuccessfulRemoteDataObject( + item ) + }), + queryParams: observableOf({ + bitstream : bitstream.uuid }) }; + bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { + findById: createSuccessfulRemoteDataObject$(bitstream) + }); + router = new RouterStub(); } @@ -109,6 +111,7 @@ describe('BitstreamRequestACopyPageComponent', () => { {provide: ItemRequestDataService, useValue: itemRequestDataService}, {provide: NotificationsService, useValue: notificationsService}, {provide: DSONameService, useValue: new DSONameServiceMock()}, + {provide: BitstreamDataService, useValue: bitstreamDataService}, ] }) .compileComponents(); @@ -143,7 +146,7 @@ describe('BitstreamRequestACopyPageComponent', () => { it('show the form with no values filled in based on the user', () => { expect(component.name.value).toEqual(''); expect(component.email.value).toEqual(''); - expect(component.allfiles.value).toEqual('true'); + expect(component.allfiles.value).toEqual('false'); expect(component.message.value).toEqual(''); }); }); @@ -163,8 +166,38 @@ describe('BitstreamRequestACopyPageComponent', () => { fixture.detectChanges(); expect(component.name.value).toEqual(eperson.name); expect(component.email.value).toEqual(eperson.email); + expect(component.allfiles.value).toEqual('false'); + expect(component.message.value).toEqual(''); + }); + }); + describe('when no bitstream was provided', () => { + beforeEach(waitForAsync(() => { + init(); + activatedRoute = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject( + item + ) + }), + queryParams: observableOf({ + }) + }; + initTestbed(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should set the all files value to true and disable the false value', () => { + expect(component.name.value).toEqual(''); + expect(component.email.value).toEqual(''); expect(component.allfiles.value).toEqual('true'); expect(component.message.value).toEqual(''); + + const allFilesFalse = fixture.debugElement.query(By.css('#allfiles-false')).nativeElement; + expect(allFilesFalse.getAttribute('disabled')).toBeTruthy(); + }); }); describe('when the user has authorization to download the file', () => { diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts index 35b56e5434..7697d5fad8 100644 --- a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { map, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../empty.util'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; @@ -17,9 +17,10 @@ import { ItemRequestDataService } from '../../core/data/item-request-data.servic import { ItemRequest } from '../../core/shared/item-request.model'; import { Item } from '../../core/shared/item.model'; import { NotificationsService } from '../notifications/notifications.service'; -import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Location } from '@angular/common'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; @Component({ selector: 'ds-bitstream-request-a-copy-page', @@ -30,15 +31,19 @@ import { Location } from '@angular/common'; */ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { - bitstream$: Observable; + item$: Observable; canDownload$: Observable; private subs: Subscription[] = []; requestCopyForm: FormGroup; - bitstream: Bitstream; + item: Item; itemName: string; + bitstream$: Observable; + bitstream: Bitstream; + bitstreamName: string; + constructor(private location: Location, private translateService: TranslateService, private route: ActivatedRoute, @@ -49,6 +54,7 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { private itemRequestDataService: ItemRequestDataService, private notificationsService: NotificationsService, private dsoNameService: DSONameService, + private bitstreamService: BitstreamDataService, ) { } @@ -66,23 +72,25 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { }); - this.bitstream$ = this.route.data.pipe( - map((data) => data.bitstream), + this.item$ = this.route.data.pipe( + map((data) => data.dso), + getFirstSucceededRemoteDataPayload() + ); + + this.subs.push(this.item$.subscribe((item) => { + this.item = item; + this.itemName = this.dsoNameService.getName(item); + })); + + this.bitstream$ = this.route.queryParams.pipe( + filter((params) => hasValue(params) && hasValue(params.bitstream)), + switchMap((params) => this.bitstreamService.findById(params.bitstream)), getFirstSucceededRemoteDataPayload() ); this.subs.push(this.bitstream$.subscribe((bitstream) => { this.bitstream = bitstream; - })); - - this.subs.push(this.bitstream$.pipe( - switchMap((bitstream) => bitstream.bundle), - getFirstSucceededRemoteDataPayload(), - switchMap((bundle) => bundle.item), - getFirstSucceededRemoteDataPayload(), - ).subscribe((item) => { - this.item = item; - this.itemName = this.dsoNameService.getName(item); + this.bitstreamName = this.dsoNameService.getName(bitstream); })); this.canDownload$ = this.bitstream$.pipe( @@ -124,9 +132,11 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { this.requestCopyForm.patchValue({allfiles: 'true'}); if (hasValue(user)) { this.requestCopyForm.patchValue({name: user.name, email: user.email}); - console.log('ping'); } }); + this.bitstream$.pipe(take(1)).subscribe((bitstream) => { + this.requestCopyForm.patchValue({allfiles: 'false'}); + }); } /** @@ -152,10 +162,10 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { */ onSubmit() { const itemRequest = new ItemRequest(); - if (hasValue(this.item)) { - itemRequest.itemId = this.item.uuid; + if (hasValue(this.bitstream)) { + itemRequest.bitstreamId = this.bitstream.uuid; } - itemRequest.bitstreamId = this.bitstream.uuid; + itemRequest.itemId = this.item.uuid; itemRequest.allfiles = this.allfiles.value; itemRequest.requestEmail = this.email.value; itemRequest.requestName = this.name.value; @@ -190,6 +200,10 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { this.location.back(); } + getItemPath() { + return [getItemPageRoute(this.item)]; + } + /** * Retrieves the link to the bistream download page */ diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index 80a30e3bdd..cdf4f30837 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -7,6 +7,8 @@ import { getBitstreamModuleRoute } from '../../app-routing-paths'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { cold, getTestScheduler } from 'jasmine-marbles'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { Item } from '../../core/shared/item.model'; +import { getItemModuleRoute } from '../../item-page/item-page-routing-paths'; describe('FileDownloadLinkComponent', () => { let component: FileDownloadLinkComponent; @@ -16,6 +18,7 @@ describe('FileDownloadLinkComponent', () => { let authorizationService: AuthorizationDataService; let bitstream: Bitstream; + let item: Item; function init() { authorizationService = jasmine.createSpyObj('authorizationService', { @@ -27,6 +30,12 @@ describe('FileDownloadLinkComponent', () => { self: {href: 'obj-selflink'} } }); + item = Object.assign(new Item(), { + uuid: 'itemUuid', + _links: { + self: {href: 'obj-selflink'} + } + }); } function initTestbed() { @@ -52,6 +61,7 @@ describe('FileDownloadLinkComponent', () => { fixture = TestBed.createComponent(FileDownloadLinkComponent); component = fixture.componentInstance; component.bitstream = bitstream; + component.item = item; fixture.detectChanges(); }); it('should return the bitstreamPath based on the input bitstream', () => { @@ -83,11 +93,12 @@ describe('FileDownloadLinkComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(FileDownloadLinkComponent); component = fixture.componentInstance; + component.item = item; component.bitstream = bitstream; fixture.detectChanges(); }); it('should return the bitstreamPath based on the input bitstream', () => { - expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'request-a-copy').toString()})); + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: `${new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()}?bitstream=${bitstream.uuid}`})); expect(component.canDownload$).toBeObservable(cold('--a', {a: false})); }); @@ -95,7 +106,7 @@ describe('FileDownloadLinkComponent', () => { scheduler.flush(); fixture.detectChanges(); const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'request-a-copy').toString()); + expect(link.href).toContain(`${new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()}?bitstream=${bitstream.uuid}`); const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; expect(lock).toBeTruthy(); }); @@ -111,6 +122,7 @@ describe('FileDownloadLinkComponent', () => { fixture = TestBed.createComponent(FileDownloadLinkComponent); component = fixture.componentInstance; component.bitstream = bitstream; + component.item = item; fixture.detectChanges(); }); it('should return the bitstreamPath based on the input bitstream', () => { diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index 2d98c97821..40d871109a 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -3,9 +3,10 @@ import { Bitstream } from '../../core/shared/bitstream.model'; import { getBitstreamDownloadRoute, getBitstreamRequestACopyRoute } from '../../app-routing-paths'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { isNotEmpty } from '../empty.util'; +import { hasValue, isNotEmpty } from '../empty.util'; import { map } from 'rxjs/operators'; import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { Item } from '../../core/shared/item.model'; @Component({ selector: 'ds-file-download-link', @@ -24,6 +25,8 @@ export class FileDownloadLinkComponent implements OnInit { */ @Input() bitstream: Bitstream; + @Input() item: Item; + /** * Additional css classes to apply to link */ @@ -59,8 +62,8 @@ export class FileDownloadLinkComponent implements OnInit { } getBitstreamPath(canDownload: boolean, canRequestACopy: boolean) { - if (!canDownload && canRequestACopy) { - return getBitstreamRequestACopyRoute(this.bitstream); + if (!canDownload && canRequestACopy && hasValue(this.item)) { + return getBitstreamRequestACopyRoute(this.item, this.bitstream); } return getBitstreamDownloadRoute(this.bitstream); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2448570ab4..d83ef56a5a 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -594,7 +594,10 @@ "bitstream-request-a-copy.header": "Request a copy of the file", - "bitstream-request-a-copy.intro": "Enter the following information to request a copy", + "bitstream-request-a-copy.intro": "Enter the following information to request a copy for the following item: ", + + "bitstream-request-a-copy.intro.bitstream.one": "Requesting the following file: ", + "bitstream-request-a-copy.intro.bitstream.all": "Requesting all files. ", "bitstream-request-a-copy.name.label": "Name *", From 90b706239e86fd6171730ef7285cbe819a868e94 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 30 Sep 2021 16:28:49 +0200 Subject: [PATCH 03/12] Fix page reload when requesting a copy --- .../file-download-link.component.html | 2 +- .../file-download-link.component.spec.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index fc4b3b97ac..0135f74745 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index cdf4f30837..8b39b93dc6 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -9,6 +9,7 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { Item } from '../../core/shared/item.model'; import { getItemModuleRoute } from '../../item-page/item-page-routing-paths'; +import { RouterLinkDirectiveStub } from '../testing/router-link-directive.stub'; describe('FileDownloadLinkComponent', () => { let component: FileDownloadLinkComponent; @@ -40,7 +41,7 @@ describe('FileDownloadLinkComponent', () => { function initTestbed() { TestBed.configureTestingModule({ - declarations: [FileDownloadLinkComponent], + declarations: [FileDownloadLinkComponent, RouterLinkDirectiveStub], providers: [ {provide: AuthorizationDataService, useValue: authorizationService}, ] @@ -72,8 +73,8 @@ describe('FileDownloadLinkComponent', () => { it('should init the component', () => { scheduler.flush(); fixture.detectChanges(); - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); + const link = fixture.debugElement.query(By.css('a')); + expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); const lock = fixture.debugElement.query(By.css('.fa-lock')); expect(lock).toBeNull(); }); @@ -105,8 +106,8 @@ describe('FileDownloadLinkComponent', () => { it('should init the component', () => { scheduler.flush(); fixture.detectChanges(); - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain(`${new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()}?bitstream=${bitstream.uuid}`); + const link = fixture.debugElement.query(By.css('a')); + expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(`${new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()}?bitstream=${bitstream.uuid}`); const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; expect(lock).toBeTruthy(); }); @@ -133,8 +134,8 @@ describe('FileDownloadLinkComponent', () => { it('should init the component', () => { scheduler.flush(); fixture.detectChanges(); - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); + const link = fixture.debugElement.query(By.css('a')); + expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; expect(lock).toBeTruthy(); }); From 4feccb99890ab54d970a6f839b984a4fe559e24e Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 30 Sep 2021 17:01:30 +0200 Subject: [PATCH 04/12] intermittent commit --- .../grant-deny-request-copy.component.html | 20 ++++++++ .../grant-deny-request-copy.component.scss | 0 .../grant-deny-request-copy.component.ts | 50 +++++++++++++++++++ .../request-copy-routing.module.ts | 27 ++++++++++ src/app/request-copy/request-copy.module.ts | 22 ++++++++ src/app/request-copy/request-copy.resolver.ts | 19 +++++++ 6 files changed, 138 insertions(+) create mode 100644 src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html create mode 100644 src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.scss create mode 100644 src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts create mode 100644 src/app/request-copy/request-copy-routing.module.ts create mode 100644 src/app/request-copy/request-copy.module.ts create mode 100644 src/app/request-copy/request-copy.resolver.ts diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html new file mode 100644 index 0000000000..ae38a964e8 --- /dev/null +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html @@ -0,0 +1,20 @@ +
+

{{'grant-deny-request-copy.header' | translate}}

+

{{'grant-deny-request-copy.intro' | translate}}

+ + + + +
\ No newline at end of file diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.scss b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts new file mode 100644 index 0000000000..f8134cdf96 --- /dev/null +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts @@ -0,0 +1,50 @@ +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 } from 'rxjs/operators'; +import { Registration } from '../../core/shared/registration.model'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { getFirstCompletedRemoteData, redirectOn4xx } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { AuthService } from '../../core/auth/auth.service'; + +@Component({ + selector: 'ds-grant-deny-request-copy', + styleUrls: ['./grant-deny-request-copy.component.scss'], + templateUrl: './grant-deny-request-copy.component.html' +}) +export class GrantDenyRequestCopyComponent implements OnInit { + private itemRequest$: Observable>; + + + constructor( + private translateService: TranslateService, + private ePersonDataService: EPersonDataService, + private store: Store, + private router: Router, + private route: ActivatedRoute, + private formBuilder: FormBuilder, + private notificationsService: NotificationsService, + private endUserAgreementService: EndUserAgreementService, + private authService: AuthService + ) { + + } + + ngOnInit(): void { + this.itemRequest$ = this.route.data.pipe( + map((data) => data.request as RemoteData), + getFirstCompletedRemoteData(), + redirectOn4xx(this.router, this.authService) + ); + + } + +} diff --git a/src/app/request-copy/request-copy-routing.module.ts b/src/app/request-copy/request-copy-routing.module.ts new file mode 100644 index 0000000000..734a661014 --- /dev/null +++ b/src/app/request-copy/request-copy-routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard'; +import { RequestCopyResolver } from './request-copy.resolver'; +import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: ':token', + component: GrantDenyRequestCopyComponent, + resolve: {request: RequestCopyResolver}, + canActivate: [EndUserAgreementCookieGuard] + } + ]) + ], + providers: [ + RequestCopyResolver, + GrantDenyRequestCopyComponent + ] +}) +/** + * Module related to the navigation to components used to register a new user + */ +export class RegisterPageRoutingModule { +} diff --git a/src/app/request-copy/request-copy.module.ts b/src/app/request-copy/request-copy.module.ts new file mode 100644 index 0000000000..eef0ee4313 --- /dev/null +++ b/src/app/request-copy/request-copy.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + GrantDenyRequestCopyComponent + ], + providers: [] +}) + +/** + * Module related to components used to register a new user + */ +export class RequestCopyModule { + +} diff --git a/src/app/request-copy/request-copy.resolver.ts b/src/app/request-copy/request-copy.resolver.ts new file mode 100644 index 0000000000..0086d51e57 --- /dev/null +++ b/src/app/request-copy/request-copy.resolver.ts @@ -0,0 +1,19 @@ +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { RemoteData } from '../core/data/remote-data'; +import { ItemRequest } from '../core/shared/item-request.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { ItemRequestDataService } from '../core/data/item-request-data.service'; + +export class RequestCopyResolver implements Resolve> { + + constructor( + private itemRequestDataService: ItemRequestDataService, + ) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> | Promise> | RemoteData { + // TODO add method after knowing whether they will change the rest object to be compatible with normal dataservice. + return undefined; + } + +} \ No newline at end of file From 11bf10cbde29c20e6f0053dbcb9a4be2a807195c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 4 Oct 2021 17:53:49 +0200 Subject: [PATCH 05/12] 83635: Intermediate commit --- src/app/app-routing-paths.ts | 5 ++ src/app/app-routing.module.ts | 6 ++- .../core/data/item-request-data.service.ts | 18 ++++++- src/app/core/shared/item-request.model.ts | 15 +++++- .../deny-request-copy.component.html | 9 ++++ .../deny-request-copy.component.scss | 0 .../deny-request-copy.component.ts | 37 ++++++++++++++ .../email-request-copy.component.html | 29 +++++++++++ .../email-request-copy.component.scss | 0 .../email-request-copy.component.ts | 26 ++++++++++ .../request-copy-email.model.ts | 5 ++ .../grant-deny-request-copy.component.html | 34 +++++++------ .../grant-deny-request-copy.component.ts | 51 ++++++++++++++----- .../request-copy-routing-paths.ts | 18 +++++++ .../request-copy-routing.module.ts | 24 ++++++--- src/app/request-copy/request-copy.module.ts | 8 ++- src/app/request-copy/request-copy.resolver.ts | 10 ++-- src/assets/i18n/en.json5 | 31 +++++++++++ 18 files changed, 279 insertions(+), 47 deletions(-) create mode 100644 src/app/request-copy/deny-request-copy/deny-request-copy.component.html create mode 100644 src/app/request-copy/deny-request-copy/deny-request-copy.component.scss create mode 100644 src/app/request-copy/deny-request-copy/deny-request-copy.component.ts create mode 100644 src/app/request-copy/email-request-copy/email-request-copy.component.html create mode 100644 src/app/request-copy/email-request-copy/email-request-copy.component.scss create mode 100644 src/app/request-copy/email-request-copy/email-request-copy.component.ts create mode 100644 src/app/request-copy/email-request-copy/request-copy-email.model.ts create mode 100644 src/app/request-copy/request-copy-routing-paths.ts diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 74a3f7183e..3fa56698f7 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -94,3 +94,8 @@ export const ACCESS_CONTROL_MODULE_PATH = 'access-control'; export function getAccessControlModuleRoute() { return `/${ACCESS_CONTROL_MODULE_PATH}`; } + +export const REQUEST_COPY_MODULE_PATH = 'request-a-copy'; +export function getRequestCopyModulePath() { + return `/${REQUEST_COPY_MODULE_PATH}`; +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 52a07b89f5..fa75b0b2f0 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -14,7 +14,7 @@ import { PROFILE_MODULE_PATH, REGISTER_PATH, WORKFLOW_ITEM_MODULE_PATH, - LEGACY_BITSTREAM_MODULE_PATH, + LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH, } from './app-routing-paths'; import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths'; @@ -180,6 +180,10 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu path: INFO_MODULE_PATH, loadChildren: () => import('./info/info.module').then((m) => m.InfoModule), }, + { + path: REQUEST_COPY_MODULE_PATH, + loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule), + }, { path: FORBIDDEN_PATH, component: ThemedForbiddenComponent diff --git a/src/app/core/data/item-request-data.service.ts b/src/app/core/data/item-request-data.service.ts index dc11e2b0c5..a0a7fe6d03 100644 --- a/src/app/core/data/item-request-data.service.ts +++ b/src/app/core/data/item-request-data.service.ts @@ -9,6 +9,13 @@ import { PostRequest } from './request.models'; import { RequestService } from './request.service'; import { ItemRequest } from '../shared/item-request.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { DataService } from './data.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; /** * A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint @@ -18,14 +25,21 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util'; providedIn: 'root', } ) -export class ItemRequestDataService { +export class ItemRequestDataService extends DataService { protected linkPath = 'itemrequests'; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected halService: HALEndpointService) { + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer, + ) { + super(); } getItemRequestEndpoint(): Observable { diff --git a/src/app/core/shared/item-request.model.ts b/src/app/core/shared/item-request.model.ts index b14ed07656..da9c4c9e08 100644 --- a/src/app/core/shared/item-request.model.ts +++ b/src/app/core/shared/item-request.model.ts @@ -1,14 +1,16 @@ -import { autoserialize } from 'cerialize'; +import { autoserialize, deserialize } from 'cerialize'; import { typedObject } from '../cache/builders/build-decorators'; import { excludeFromEquals } from '../utilities/equals.decorators'; import { ResourceType } from './resource-type'; import { ITEM_REQUEST } from './item-request.resource-type'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { HALLink } from './hal-link.model'; /** * Model class for a Configuration Property */ @typedObject -export class ItemRequest { +export class ItemRequest implements CacheableObject { static type = ITEM_REQUEST; /** @@ -75,5 +77,14 @@ export class ItemRequest { @autoserialize bitstreamId: string; + /** + * The {@link HALLink}s for this ItemRequest + */ + @deserialize + _links: { + self: HALLink; + item: HALLink; + bitstream: HALLink; + }; } diff --git a/src/app/request-copy/deny-request-copy/deny-request-copy.component.html b/src/app/request-copy/deny-request-copy/deny-request-copy.component.html new file mode 100644 index 0000000000..226fac1f67 --- /dev/null +++ b/src/app/request-copy/deny-request-copy/deny-request-copy.component.html @@ -0,0 +1,9 @@ +
+

{{'deny-request-copy.header' | translate}}

+
+

{{'deny-request-copy.intro' | translate}}

+ + +
+ +
diff --git a/src/app/request-copy/deny-request-copy/deny-request-copy.component.scss b/src/app/request-copy/deny-request-copy/deny-request-copy.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts b/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts new file mode 100644 index 0000000000..4ffeb61605 --- /dev/null +++ b/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { + getFirstCompletedRemoteData, + redirectOn4xx +} from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { AuthService } from '../../core/auth/auth.service'; + +@Component({ + selector: 'ds-deny-request-copy', + styleUrls: ['./deny-request-copy.component.scss'], + templateUrl: './deny-request-copy.component.html' +}) +export class DenyRequestCopyComponent implements OnInit { + itemRequestRD$: Observable>; + + constructor( + private router: Router, + private route: ActivatedRoute, + private authService: AuthService, + ) { + + } + + ngOnInit(): void { + this.itemRequestRD$ = this.route.data.pipe( + map((data) => data.request as RemoteData), + getFirstCompletedRemoteData(), + redirectOn4xx(this.router, this.authService), + ); + } + +} diff --git a/src/app/request-copy/email-request-copy/email-request-copy.component.html b/src/app/request-copy/email-request-copy/email-request-copy.component.html new file mode 100644 index 0000000000..b4c7b65aba --- /dev/null +++ b/src/app/request-copy/email-request-copy/email-request-copy.component.html @@ -0,0 +1,29 @@ + +
+ + +
+ {{ 'grant-deny-request-copy.email.subject.empty' | translate }} +
+
+
+ + +
+ {{ 'grant-deny-request-copy.email.message.empty' | translate }} +
+
+
+ + +
+ diff --git a/src/app/request-copy/email-request-copy/email-request-copy.component.scss b/src/app/request-copy/email-request-copy/email-request-copy.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/request-copy/email-request-copy/email-request-copy.component.ts b/src/app/request-copy/email-request-copy/email-request-copy.component.ts new file mode 100644 index 0000000000..0bb2286b4d --- /dev/null +++ b/src/app/request-copy/email-request-copy/email-request-copy.component.ts @@ -0,0 +1,26 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { RequestCopyEmail } from './request-copy-email.model'; +import { Location } from '@angular/common'; + +@Component({ + selector: 'ds-email-request-copy', + styleUrls: ['./email-request-copy.component.scss'], + templateUrl: './email-request-copy.component.html' +}) +export class EmailRequestCopyComponent { + @Output() send: EventEmitter = new EventEmitter(); + + @Input() subject: string; + @Input() message: string; + + constructor(protected location: Location) { + } + + submit() { + this.send.emit(new RequestCopyEmail(this.subject, this.message)); + } + + return() { + this.location.back(); + } +} diff --git a/src/app/request-copy/email-request-copy/request-copy-email.model.ts b/src/app/request-copy/email-request-copy/request-copy-email.model.ts new file mode 100644 index 0000000000..45cb44e1ce --- /dev/null +++ b/src/app/request-copy/email-request-copy/request-copy-email.model.ts @@ -0,0 +1,5 @@ +export class RequestCopyEmail { + constructor(public subject: string, + public message: string) { + } +} diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html index ae38a964e8..5be182c4cb 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html @@ -1,20 +1,22 @@ -
-

{{'grant-deny-request-copy.header' | translate}}

-

{{'grant-deny-request-copy.intro' | translate}}

- +
+

{{'grant-deny-request-copy.header' | translate}}

+ \ No newline at end of file +
+ +
diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts index f8134cdf96..81e5c8abac 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts @@ -7,13 +7,20 @@ 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 } from 'rxjs/operators'; -import { Registration } from '../../core/shared/registration.model'; +import { map, switchMap } from 'rxjs/operators'; import { ItemRequest } from '../../core/shared/item-request.model'; import { Observable } from 'rxjs/internal/Observable'; -import { getFirstCompletedRemoteData, redirectOn4xx } from '../../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, + redirectOn4xx +} from '../../core/shared/operators'; import { RemoteData } from '../../core/data/remote-data'; import { AuthService } from '../../core/auth/auth.service'; +import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths'; +import { Item } from '../../core/shared/item.model'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-grant-deny-request-copy', @@ -21,30 +28,46 @@ import { AuthService } from '../../core/auth/auth.service'; templateUrl: './grant-deny-request-copy.component.html' }) export class GrantDenyRequestCopyComponent implements OnInit { - private itemRequest$: Observable>; - + itemRequestRD$: Observable>; + itemRD$: Observable>; + itemName$: Observable; + + denyRoute$: Observable; + grantRoute$: Observable; constructor( - private translateService: TranslateService, - private ePersonDataService: EPersonDataService, - private store: Store, private router: Router, private route: ActivatedRoute, - private formBuilder: FormBuilder, - private notificationsService: NotificationsService, - private endUserAgreementService: EndUserAgreementService, - private authService: AuthService + private authService: AuthService, + private itemDataService: ItemDataService, + private nameService: DSONameService, ) { } ngOnInit(): void { - this.itemRequest$ = this.route.data.pipe( + this.itemRequestRD$ = this.route.data.pipe( map((data) => data.request as RemoteData), getFirstCompletedRemoteData(), - redirectOn4xx(this.router, this.authService) + redirectOn4xx(this.router, this.authService), + ); + this.itemRD$ = this.itemRequestRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((itemRequest: ItemRequest) => this.itemDataService.findById(itemRequest.itemId)), + ); + this.itemName$ = this.itemRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((item) => this.nameService.getName(item)), ); + this.denyRoute$ = this.itemRequestRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((itemRequest: ItemRequest) => getRequestCopyDenyRoute(itemRequest.token)) + ); + this.grantRoute$ = this.itemRequestRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((itemRequest: ItemRequest) => getRequestCopyGrantRoute(itemRequest.token)) + ); } } diff --git a/src/app/request-copy/request-copy-routing-paths.ts b/src/app/request-copy/request-copy-routing-paths.ts new file mode 100644 index 0000000000..1d0204a1b8 --- /dev/null +++ b/src/app/request-copy/request-copy-routing-paths.ts @@ -0,0 +1,18 @@ +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { getRequestCopyModulePath } from '../app-routing-paths'; + +export function getRequestCopyRoute(token: string) { + return new URLCombiner(getRequestCopyModulePath(), token).toString(); +} + +export const REQUEST_COPY_DENY_PATH = 'deny'; + +export function getRequestCopyDenyRoute(token: string) { + return new URLCombiner(getRequestCopyRoute(token), REQUEST_COPY_DENY_PATH).toString(); +} + +export const REQUEST_COPY_GRANT_PATH = 'grant'; + +export function getRequestCopyGrantRoute(token: string) { + return new URLCombiner(getRequestCopyRoute(token), REQUEST_COPY_GRANT_PATH).toString(); +} diff --git a/src/app/request-copy/request-copy-routing.module.ts b/src/app/request-copy/request-copy-routing.module.ts index 734a661014..c25afcc459 100644 --- a/src/app/request-copy/request-copy-routing.module.ts +++ b/src/app/request-copy/request-copy-routing.module.ts @@ -1,17 +1,28 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard'; 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'; @NgModule({ imports: [ RouterModule.forChild([ { path: ':token', - component: GrantDenyRequestCopyComponent, - resolve: {request: RequestCopyResolver}, - canActivate: [EndUserAgreementCookieGuard] + resolve: { + request: RequestCopyResolver + }, + children: [ + { + path: '', + component: GrantDenyRequestCopyComponent, + }, + { + path: REQUEST_COPY_DENY_PATH, + component: DenyRequestCopyComponent, + } + ] } ]) ], @@ -20,8 +31,5 @@ import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-d GrantDenyRequestCopyComponent ] }) -/** - * Module related to the navigation to components used to register a new user - */ -export class RegisterPageRoutingModule { +export class RequestCopyRoutingModule { } diff --git a/src/app/request-copy/request-copy.module.ts b/src/app/request-copy/request-copy.module.ts index eef0ee4313..f3b29d5b77 100644 --- a/src/app/request-copy/request-copy.module.ts +++ b/src/app/request-copy/request-copy.module.ts @@ -2,14 +2,20 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component'; +import { RequestCopyRoutingModule } from './request-copy-routing.module'; +import { DenyRequestCopyComponent } from './deny-request-copy/deny-request-copy.component'; +import { EmailRequestCopyComponent } from './email-request-copy/email-request-copy.component'; @NgModule({ imports: [ CommonModule, SharedModule, + RequestCopyRoutingModule ], declarations: [ - GrantDenyRequestCopyComponent + GrantDenyRequestCopyComponent, + DenyRequestCopyComponent, + EmailRequestCopyComponent, ], providers: [] }) diff --git a/src/app/request-copy/request-copy.resolver.ts b/src/app/request-copy/request-copy.resolver.ts index 0086d51e57..948445ee0f 100644 --- a/src/app/request-copy/request-copy.resolver.ts +++ b/src/app/request-copy/request-copy.resolver.ts @@ -3,7 +3,10 @@ import { RemoteData } from '../core/data/remote-data'; import { ItemRequest } from '../core/shared/item-request.model'; import { Observable } from 'rxjs/internal/Observable'; import { ItemRequestDataService } from '../core/data/item-request-data.service'; +import { Injectable } from '@angular/core'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +@Injectable() export class RequestCopyResolver implements Resolve> { constructor( @@ -12,8 +15,9 @@ export class RequestCopyResolver implements Resolve> { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> | Promise> | RemoteData { - // TODO add method after knowing whether they will change the rest object to be compatible with normal dataservice. - return undefined; + return this.itemRequestDataService.findById(route.params.token).pipe( + getFirstCompletedRemoteData(), + ); } -} \ No newline at end of file +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d83ef56a5a..1b7598a7d1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1206,6 +1206,12 @@ + "deny-request-copy.header": "Deny document copy request", + + "deny-request-copy.intro": "This message will be sent to the applicant of the request", + + + "dso.name.untitled": "Untitled", @@ -1430,6 +1436,31 @@ "form.repeatable.sort.tip": "Drop the item in the new position", + + "grant-deny-request-copy.deny": "Don't send copy", + + "grant-deny-request-copy.email.back": "Back", + + "grant-deny-request-copy.email.message": "Message", + + "grant-deny-request-copy.email.message.empty": "Please enter a message", + + "grant-deny-request-copy.email.send": "Send", + + "grant-deny-request-copy.email.subject": "Subject", + + "grant-deny-request-copy.email.subject.empty": "Please enter a subject", + + "grant-deny-request-copy.grant": "Send copy", + + "grant-deny-request-copy.header": "Document copy request", + + "grant-deny-request-copy.intro1": "IF YOU ARE THE AUTHOR (OR AN AUTHOR) OF DOCUMENT \"{{ name }}\" use the buttons to answer the user's request.", + + "grant-deny-request-copy.intro2": "This repository will propose an appropriate model reply, which you may edit.", + + + "home.description": "", "home.breadcrumbs": "Home", From 120b9f5ce94d39fad4ee7e57baeee467b74077a6 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 5 Oct 2021 14:55:34 +0200 Subject: [PATCH 06/12] 83635: Grant/Deny item requests --- src/app/app-routing.module.ts | 1 + .../core/data/item-request-data.service.ts | 72 +++++++++-- .../deny-request-copy.component.html | 2 +- .../deny-request-copy.component.ts | 79 +++++++++++- .../email-request-copy.component.html | 7 +- .../email-request-copy.component.ts | 19 +++ .../request-copy-email.model.ts | 3 + .../grant-deny-request-copy.component.ts | 28 +++-- .../grant-request-copy.component.html | 17 +++ .../grant-request-copy.component.scss | 0 .../grant-request-copy.component.ts | 118 ++++++++++++++++++ .../request-copy-routing.module.ts | 7 +- src/app/request-copy/request-copy.module.ts | 4 +- src/app/request-copy/request-copy.resolver.ts | 3 + src/assets/i18n/en.json5 | 26 ++++ 15 files changed, 364 insertions(+), 22 deletions(-) create mode 100644 src/app/request-copy/grant-request-copy/grant-request-copy.component.html create mode 100644 src/app/request-copy/grant-request-copy/grant-request-copy.component.scss create mode 100644 src/app/request-copy/grant-request-copy/grant-request-copy.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index fa75b0b2f0..157ada622d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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, diff --git a/src/app/core/data/item-request-data.service.ts b/src/app/core/data/item-request-data.service.ts index a0a7fe6d03..41ad19211a 100644 --- a/src/app/core/data/item-request-data.service.ts +++ b/src/app/core/data/item-request-data.service.ts @@ -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 { return this.halService.getEndpoint(this.linkPath); } - getFindItemRequestEndpoint(requestID: string): Observable { + /** + * Get the endpoint for an {@link ItemRequest} by their token + * @param token + */ + getItemRequestEndpointByToken(token: string): Observable { 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> { const requestId = this.requestService.generateRequestId(); @@ -70,4 +80,52 @@ export class ItemRequestDataService extends DataService { ); } + /** + * 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> { + 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> { + 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> { + 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); + } + } diff --git a/src/app/request-copy/deny-request-copy/deny-request-copy.component.html b/src/app/request-copy/deny-request-copy/deny-request-copy.component.html index 226fac1f67..b00bc079dd 100644 --- a/src/app/request-copy/deny-request-copy/deny-request-copy.component.html +++ b/src/app/request-copy/deny-request-copy/deny-request-copy.component.html @@ -3,7 +3,7 @@

{{'deny-request-copy.intro' | translate}}

- +
diff --git a/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts b/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts index 4ffeb61605..0795cf5919 100644 --- a/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts +++ b/src/app/request-copy/deny-request-copy/deny-request-copy.component.ts @@ -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>; + /** + * The default subject of the message to send to the user requesting the item + */ + subject$: Observable; + /** + * The default contents of the message to send to the user requesting the item + */ + message$: Observable; + 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); + } + }); } } diff --git a/src/app/request-copy/email-request-copy/email-request-copy.component.html b/src/app/request-copy/email-request-copy/email-request-copy.component.html index b4c7b65aba..6f5bf9189b 100644 --- a/src/app/request-copy/email-request-copy/email-request-copy.component.html +++ b/src/app/request-copy/email-request-copy/email-request-copy.component.html @@ -8,17 +8,18 @@
- +
{{ 'grant-deny-request-copy.email.message.empty' | translate }}
+
-
+
diff --git a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts index 7697d5fad8..3b9bc38aab 100644 --- a/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts +++ b/src/app/shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../empty.util'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 64bcde7167..6d9ae075a2 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1453,7 +1453,7 @@ "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.info": "You may use this occasion to reconsider the access restrictions on the document, to avoid having to respond to these requests. If you’d like to ask the repository administrators to remove these restrictions, please check the box below.", "grant-deny-request-copy.email.permissions.label": "Change to open access", From 881eb92fa10bbc471da2569ca1de39bd419d229a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 7 Oct 2021 13:53:02 +0200 Subject: [PATCH 08/12] 83635: Feedback and test cases --- .../data/item-request-data.service.spec.ts | 95 ++++++++++ src/app/core/shared/item-request.model.ts | 2 +- .../deny-request-copy.component.spec.ts | 177 ++++++++++++++++++ .../email-request-copy.component.spec.ts | 47 +++++ .../grant-deny-request-copy.component.html | 2 +- .../grant-deny-request-copy.component.spec.ts | 123 ++++++++++++ .../grant-deny-request-copy.component.ts | 10 + .../grant-request-copy.component.spec.ts | 177 ++++++++++++++++++ src/assets/i18n/en.json5 | 8 +- 9 files changed, 635 insertions(+), 6 deletions(-) create mode 100644 src/app/core/data/item-request-data.service.spec.ts create mode 100644 src/app/request-copy/deny-request-copy/deny-request-copy.component.spec.ts create mode 100644 src/app/request-copy/email-request-copy/email-request-copy.component.spec.ts create mode 100644 src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts create mode 100644 src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts diff --git a/src/app/core/data/item-request-data.service.spec.ts b/src/app/core/data/item-request-data.service.spec.ts new file mode 100644 index 0000000000..0d99ca5cd4 --- /dev/null +++ b/src/app/core/data/item-request-data.service.spec.ts @@ -0,0 +1,95 @@ +import { ItemRequestDataService } from './item-request-data.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { ItemRequest } from '../shared/item-request.model'; +import { PostRequest } from './request.models'; +import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; +import { RestRequestMethod } from './rest-request-method'; + +describe('ItemRequestDataService', () => { + let service: ItemRequestDataService; + + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let halService: HALEndpointService; + + const restApiEndpoint = 'rest/api/endpoint/'; + const requestId = 'request-id'; + let itemRequest: ItemRequest; + + beforeEach(() => { + itemRequest = Object.assign(new ItemRequest(), { + token: 'item-request-token', + }); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestId, + send: '', + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildFromRequestUUID: createSuccessfulRemoteDataObject$(itemRequest), + }); + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(restApiEndpoint), + }); + + service = new ItemRequestDataService(requestService, rdbService, null, null, halService, null, null, null); + }); + + describe('requestACopy', () => { + it('should send a POST request containing the provided item request', (done) => { + service.requestACopy(itemRequest).subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestId, restApiEndpoint, itemRequest)); + done(); + }); + }); + }); + + describe('grant', () => { + let email: RequestCopyEmail; + + beforeEach(() => { + email = new RequestCopyEmail('subject', 'message'); + }); + + it('should send a PUT request containing the correct properties', (done) => { + service.grant(itemRequest.token, email, true).subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.PUT, + body: JSON.stringify({ + acceptRequest: true, + responseMessage: email.message, + subject: email.subject, + suggestOpenAccess: true, + }), + })); + done(); + }); + }); + }); + + describe('deny', () => { + let email: RequestCopyEmail; + + beforeEach(() => { + email = new RequestCopyEmail('subject', 'message'); + }); + + it('should send a PUT request containing the correct properties', (done) => { + service.deny(itemRequest.token, email).subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.PUT, + body: JSON.stringify({ + acceptRequest: false, + responseMessage: email.message, + subject: email.subject, + suggestOpenAccess: false, + }), + })); + done(); + }); + }); + }); +}); diff --git a/src/app/core/shared/item-request.model.ts b/src/app/core/shared/item-request.model.ts index da9c4c9e08..08b65abebf 100644 --- a/src/app/core/shared/item-request.model.ts +++ b/src/app/core/shared/item-request.model.ts @@ -7,7 +7,7 @@ import { CacheableObject } from '../cache/object-cache.reducer'; import { HALLink } from './hal-link.model'; /** - * Model class for a Configuration Property + * Model class for an ItemRequest */ @typedObject export class ItemRequest implements CacheableObject { diff --git a/src/app/request-copy/deny-request-copy/deny-request-copy.component.spec.ts b/src/app/request-copy/deny-request-copy/deny-request-copy.component.spec.ts new file mode 100644 index 0000000000..c88bfd3b5e --- /dev/null +++ b/src/app/request-copy/deny-request-copy/deny-request-copy.component.spec.ts @@ -0,0 +1,177 @@ +import { DenyRequestCopyComponent } from './deny-request-copy.component'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '../../core/auth/auth.service'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { of as observableOf } from 'rxjs'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { Item } from '../../core/shared/item.model'; +import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model'; + +describe('DenyRequestCopyComponent', () => { + let component: DenyRequestCopyComponent; + let fixture: ComponentFixture; + + let router: Router; + let route: ActivatedRoute; + let authService: AuthService; + let translateService: TranslateService; + let itemDataService: ItemDataService; + let nameService: DSONameService; + let itemRequestService: ItemRequestDataService; + let notificationsService: NotificationsService; + + let itemRequest: ItemRequest; + let user: EPerson; + let item: Item; + let itemName: string; + let itemUrl: string; + + beforeEach(waitForAsync(() => { + itemRequest = Object.assign(new ItemRequest(), { + token: 'item-request-token', + requestName: 'requester name' + }); + user = Object.assign(new EPerson(), { + metadata: { + 'eperson.firstname': [ + { + value: 'first' + } + ], + 'eperson.lastname': [ + { + value: 'last' + } + ] + }, + email: 'user-email', + }); + itemName = 'item-name'; + itemUrl = 'item-url'; + item = Object.assign(new Item(), { + id: 'item-id', + metadata: { + 'dc.identifier.uri': [ + { + value: itemUrl + } + ], + 'dc.title': [ + { + value: itemName + } + ] + } + }); + + router = jasmine.createSpyObj('router', { + navigateByUrl: jasmine.createSpy('navigateByUrl'), + }); + route = jasmine.createSpyObj('route', {}, { + data: observableOf({ + request: createSuccessfulRemoteDataObject(itemRequest), + }), + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true), + getAuthenticatedUserFromStore: observableOf(user), + }); + itemDataService = jasmine.createSpyObj('itemDataService', { + findById: createSuccessfulRemoteDataObject$(item), + }); + nameService = jasmine.createSpyObj('nameService', { + getName: itemName, + }); + itemRequestService = jasmine.createSpyObj('itemRequestService', { + deny: createSuccessfulRemoteDataObject$(itemRequest), + }); + notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']); + + TestBed.configureTestingModule({ + declarations: [DenyRequestCopyComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: route }, + { provide: AuthService, useValue: authService }, + { provide: ItemDataService, useValue: itemDataService }, + { provide: DSONameService, useValue: nameService }, + { provide: ItemRequestDataService, useValue: itemRequestService }, + { provide: NotificationsService, useValue: notificationsService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DenyRequestCopyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + translateService = (component as any).translateService; + spyOn(translateService, 'get').and.returnValue(observableOf('translated-message')); + }); + + it('message$ should be parameterized correctly', (done) => { + component.message$.subscribe(() => { + expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({ + recipientName: itemRequest.requestName, + itemUrl: itemUrl, + itemName: itemName, + authorName: user.name, + authorEmail: user.email, + })); + done(); + }); + }); + + describe('deny', () => { + let email: RequestCopyEmail; + + describe('when the request is successful', () => { + beforeEach(() => { + email = new RequestCopyEmail('subject', 'message'); + (itemRequestService.deny as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(itemRequest)); + component.deny(email); + }); + + it('should display a success notification', () => { + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should navigate to the homepage', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/'); + }); + }); + + describe('when the request is unsuccessful', () => { + beforeEach(() => { + email = new RequestCopyEmail('subject', 'message'); + (itemRequestService.deny as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); + component.deny(email); + }); + + it('should display a success notification', () => { + expect(notificationsService.error).toHaveBeenCalled(); + }); + + it('should not navigate', () => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/request-copy/email-request-copy/email-request-copy.component.spec.ts b/src/app/request-copy/email-request-copy/email-request-copy.component.spec.ts new file mode 100644 index 0000000000..3857c0d91b --- /dev/null +++ b/src/app/request-copy/email-request-copy/email-request-copy.component.spec.ts @@ -0,0 +1,47 @@ +import { EmailRequestCopyComponent } from './email-request-copy.component'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Location } from '@angular/common'; +import { RequestCopyEmail } from './request-copy-email.model'; + +describe('EmailRequestCopyComponent', () => { + let component: EmailRequestCopyComponent; + let fixture: ComponentFixture; + + let location: Location; + + beforeEach(waitForAsync(() => { + location = jasmine.createSpyObj('location', ['back']); + + TestBed.configureTestingModule({ + declarations: [EmailRequestCopyComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: Location, useValue: location }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EmailRequestCopyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('return should navigate to the previous page', () => { + component.return(); + expect(location.back).toHaveBeenCalled(); + }); + + it('submit should emit an email object', () => { + spyOn(component.send, 'emit').and.stub(); + component.subject = 'test-subject'; + component.message = 'test-message'; + component.submit(); + expect(component.send.emit).toHaveBeenCalledWith(new RequestCopyEmail('test-subject', 'test-message')); + }); +}); diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html index 5be182c4cb..abd013f9a1 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html @@ -1,7 +1,7 @@

{{'grant-deny-request-copy.header' | translate}}

-

{{'grant-deny-request-copy.intro1' | translate:{ name: (itemName$ | async) } }}

+

{{'grant-deny-request-copy.intro2' | translate}}

diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts new file mode 100644 index 0000000000..3a0817b3b0 --- /dev/null +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '../../core/auth/auth.service'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { of as observableOf } from 'rxjs'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { Item } from '../../core/shared/item.model'; +import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy.component'; +import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; +import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths'; + +describe('GrantDenyRequestCopyComponent', () => { + let component: GrantDenyRequestCopyComponent; + let fixture: ComponentFixture; + + let router: Router; + let route: ActivatedRoute; + let authService: AuthService; + let itemDataService: ItemDataService; + let nameService: DSONameService; + + let itemRequest: ItemRequest; + let item: Item; + let itemName: string; + let itemUrl: string; + + beforeEach(waitForAsync(() => { + itemRequest = Object.assign(new ItemRequest(), { + token: 'item-request-token', + requestName: 'requester name' + }); + itemName = 'item-name'; + item = Object.assign(new Item(), { + id: 'item-id', + metadata: { + 'dc.identifier.uri': [ + { + value: itemUrl + } + ], + 'dc.title': [ + { + value: itemName + } + ] + } + }); + itemUrl = getItemPageRoute(item); + + route = jasmine.createSpyObj('route', {}, { + data: observableOf({ + request: createSuccessfulRemoteDataObject(itemRequest), + }), + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true), + }); + itemDataService = jasmine.createSpyObj('itemDataService', { + findById: createSuccessfulRemoteDataObject$(item), + }); + nameService = jasmine.createSpyObj('nameService', { + getName: itemName, + }); + + TestBed.configureTestingModule({ + declarations: [GrantDenyRequestCopyComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: ActivatedRoute, useValue: route }, + { provide: AuthService, useValue: authService }, + { provide: ItemDataService, useValue: itemDataService }, + { provide: DSONameService, useValue: nameService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GrantDenyRequestCopyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + router = (component as any).router; + spyOn(router, 'navigateByUrl').and.stub(); + }); + + it('should initialise itemName$', (done) => { + component.itemName$.subscribe((result) => { + expect(result).toEqual(itemName); + done(); + }); + }); + + it('should initialise itemUrl$', (done) => { + component.itemUrl$.subscribe((result) => { + expect(result).toEqual(itemUrl); + done(); + }); + }); + + it('should initialise denyRoute$', (done) => { + component.denyRoute$.subscribe((result) => { + expect(result).toEqual(getRequestCopyDenyRoute(itemRequest.token)); + done(); + }); + }); + + it('should initialise grantRoute$', (done) => { + component.grantRoute$.subscribe((result) => { + expect(result).toEqual(getRequestCopyGrantRoute(itemRequest.token)); + done(); + }); + }); +}); diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts index fba2e3f4d0..5ba81e991b 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.ts @@ -14,6 +14,7 @@ import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-co import { Item } from '../../core/shared/item.model'; import { ItemDataService } from '../../core/data/item-data.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; @Component({ selector: 'ds-grant-deny-request-copy', @@ -39,6 +40,11 @@ export class GrantDenyRequestCopyComponent implements OnInit { */ itemName$: Observable; + /** + * The url of the item + */ + itemUrl$: Observable; + /** * The route to the page for denying access to the item */ @@ -73,6 +79,10 @@ export class GrantDenyRequestCopyComponent implements OnInit { getFirstSucceededRemoteDataPayload(), map((item) => this.nameService.getName(item)), ); + this.itemUrl$ = this.itemRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((item) => getItemPageRoute(item)), + ); this.denyRoute$ = this.itemRequestRD$.pipe( getFirstSucceededRemoteDataPayload(), diff --git a/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts b/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts new file mode 100644 index 0000000000..b6ccb8557e --- /dev/null +++ b/src/app/request-copy/grant-request-copy/grant-request-copy.component.spec.ts @@ -0,0 +1,177 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '../../core/auth/auth.service'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { ItemRequestDataService } from '../../core/data/item-request-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { of as observableOf } from 'rxjs'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; +import { ItemRequest } from '../../core/shared/item-request.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { Item } from '../../core/shared/item.model'; +import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model'; +import { GrantRequestCopyComponent } from './grant-request-copy.component'; + +describe('GrantRequestCopyComponent', () => { + let component: GrantRequestCopyComponent; + let fixture: ComponentFixture; + + let router: Router; + let route: ActivatedRoute; + let authService: AuthService; + let translateService: TranslateService; + let itemDataService: ItemDataService; + let nameService: DSONameService; + let itemRequestService: ItemRequestDataService; + let notificationsService: NotificationsService; + + let itemRequest: ItemRequest; + let user: EPerson; + let item: Item; + let itemName: string; + let itemUrl: string; + + beforeEach(waitForAsync(() => { + itemRequest = Object.assign(new ItemRequest(), { + token: 'item-request-token', + requestName: 'requester name' + }); + user = Object.assign(new EPerson(), { + metadata: { + 'eperson.firstname': [ + { + value: 'first' + } + ], + 'eperson.lastname': [ + { + value: 'last' + } + ] + }, + email: 'user-email', + }); + itemName = 'item-name'; + itemUrl = 'item-url'; + item = Object.assign(new Item(), { + id: 'item-id', + metadata: { + 'dc.identifier.uri': [ + { + value: itemUrl + } + ], + 'dc.title': [ + { + value: itemName + } + ] + } + }); + + router = jasmine.createSpyObj('router', { + navigateByUrl: jasmine.createSpy('navigateByUrl'), + }); + route = jasmine.createSpyObj('route', {}, { + data: observableOf({ + request: createSuccessfulRemoteDataObject(itemRequest), + }), + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true), + getAuthenticatedUserFromStore: observableOf(user), + }); + itemDataService = jasmine.createSpyObj('itemDataService', { + findById: createSuccessfulRemoteDataObject$(item), + }); + nameService = jasmine.createSpyObj('nameService', { + getName: itemName, + }); + itemRequestService = jasmine.createSpyObj('itemRequestService', { + grant: createSuccessfulRemoteDataObject$(itemRequest), + }); + notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']); + + TestBed.configureTestingModule({ + declarations: [GrantRequestCopyComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: route }, + { provide: AuthService, useValue: authService }, + { provide: ItemDataService, useValue: itemDataService }, + { provide: DSONameService, useValue: nameService }, + { provide: ItemRequestDataService, useValue: itemRequestService }, + { provide: NotificationsService, useValue: notificationsService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GrantRequestCopyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + translateService = (component as any).translateService; + spyOn(translateService, 'get').and.returnValue(observableOf('translated-message')); + }); + + it('message$ should be parameterized correctly', (done) => { + component.message$.subscribe(() => { + expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({ + recipientName: itemRequest.requestName, + itemUrl: itemUrl, + itemName: itemName, + authorName: user.name, + authorEmail: user.email, + })); + done(); + }); + }); + + describe('grant', () => { + let email: RequestCopyEmail; + + describe('when the request is successful', () => { + beforeEach(() => { + email = new RequestCopyEmail('subject', 'message'); + (itemRequestService.grant as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(itemRequest)); + component.grant(email); + }); + + it('should display a success notification', () => { + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should navigate to the homepage', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/'); + }); + }); + + describe('when the request is unsuccessful', () => { + beforeEach(() => { + email = new RequestCopyEmail('subject', 'message'); + (itemRequestService.grant as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); + component.grant(email); + }); + + it('should display a success notification', () => { + expect(notificationsService.error).toHaveBeenCalled(); + }); + + it('should not navigate', () => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6d9ae075a2..fb3c27883b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1206,7 +1206,7 @@ - "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.message": "Dear {{ recipientName }},\nIn response to your request I regret to inform you that it's not possible to send you a copy of the file(s) you have requested, concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", "deny-request-copy.email.subject": "Request copy of document", @@ -1467,13 +1467,13 @@ "grant-deny-request-copy.header": "Document copy request", - "grant-deny-request-copy.intro1": "IF YOU ARE THE AUTHOR (OR AN AUTHOR) OF DOCUMENT \"{{ name }}\" use the buttons to answer the user's request.", + "grant-deny-request-copy.intro1": "If you are one of the authors of the document {{ name }}, then please use one of the options below to respond to the user's request.", - "grant-deny-request-copy.intro2": "This repository will propose an appropriate model reply, which you may edit.", + "grant-deny-request-copy.intro2": "After choosing an option, you will be presented with a suggested email reply which you may edit.", - "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.message": "Dear {{ recipientName }},\nIn response to your request I have the pleasure to send you in attachment a copy of the file(s) concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", "grant-request-copy.email.subject": "Request copy of document", From a745468b0ca1aade5a0b2add8213eb6d37330028 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 14 Oct 2021 11:25:03 +0200 Subject: [PATCH 09/12] 83635: Keep bitstream param on link --- src/app/app-routing-paths.ts | 9 +++++++-- .../file-download-link.component.html | 2 +- .../file-download-link.component.spec.ts | 10 +++++----- .../file-download-link.component.ts | 16 +++++++++++++--- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 3fa56698f7..db6b22a023 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -22,9 +22,14 @@ export function getBitstreamModuleRoute() { export function getBitstreamDownloadRoute(bitstream): string { return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(); } -export function getBitstreamRequestACopyRoute(item, bitstream): string { +export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: string, queryParams: any } { const url = new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString(); - return `${url}?bitstream=${bitstream.uuid}`; + return { + routerLink: url, + queryParams: { + bitstream: bitstream.uuid + } + }; } export const ADMIN_MODULE_PATH = 'admin'; diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index 0135f74745..0155c40b0a 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index 8b39b93dc6..bf80dfcdd9 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -11,7 +11,7 @@ import { Item } from '../../core/shared/item.model'; import { getItemModuleRoute } from '../../item-page/item-page-routing-paths'; import { RouterLinkDirectiveStub } from '../testing/router-link-directive.stub'; -describe('FileDownloadLinkComponent', () => { +fdescribe('FileDownloadLinkComponent', () => { let component: FileDownloadLinkComponent; let fixture: ComponentFixture; @@ -66,7 +66,7 @@ describe('FileDownloadLinkComponent', () => { fixture.detectChanges(); }); it('should return the bitstreamPath based on the input bitstream', () => { - expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()})); + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(), queryParams: {} }})); expect(component.canDownload$).toBeObservable(cold('--a', {a: true})); }); @@ -99,7 +99,7 @@ describe('FileDownloadLinkComponent', () => { fixture.detectChanges(); }); it('should return the bitstreamPath based on the input bitstream', () => { - expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: `${new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()}?bitstream=${bitstream.uuid}`})); + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString(), queryParams: { bitstream: bitstream.uuid } }})); expect(component.canDownload$).toBeObservable(cold('--a', {a: false})); }); @@ -107,7 +107,7 @@ describe('FileDownloadLinkComponent', () => { scheduler.flush(); fixture.detectChanges(); const link = fixture.debugElement.query(By.css('a')); - expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(`${new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()}?bitstream=${bitstream.uuid}`); + expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString()); const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; expect(lock).toBeTruthy(); }); @@ -127,7 +127,7 @@ describe('FileDownloadLinkComponent', () => { fixture.detectChanges(); }); it('should return the bitstreamPath based on the input bitstream', () => { - expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()})); + expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(), queryParams: {} }})); expect(component.canDownload$).toBeObservable(cold('--a', {a: false})); }); diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index 40d871109a..a79a71b634 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -39,7 +39,10 @@ export class FileDownloadLinkComponent implements OnInit { @Input() enableRequestACopy = true; - bitstreamPath$: Observable; + bitstreamPath$: Observable<{ + routerLink: string, + queryParams: any, + }>; canDownload$: Observable; @@ -56,7 +59,7 @@ export class FileDownloadLinkComponent implements OnInit { map(([canDownload, canRequestACopy]) => this.getBitstreamPath(canDownload, canRequestACopy)) ); } else { - this.bitstreamPath$ = observableOf(getBitstreamDownloadRoute(this.bitstream)); + this.bitstreamPath$ = observableOf(this.getBitstreamDownloadPath()); this.canDownload$ = observableOf(true); } } @@ -65,6 +68,13 @@ export class FileDownloadLinkComponent implements OnInit { if (!canDownload && canRequestACopy && hasValue(this.item)) { return getBitstreamRequestACopyRoute(this.item, this.bitstream); } - return getBitstreamDownloadRoute(this.bitstream); + return this.getBitstreamDownloadPath(); + } + + getBitstreamDownloadPath() { + return { + routerLink: getBitstreamDownloadRoute(this.bitstream), + queryParams: {} + }; } } From 478c95f6d47fe88d3754cabe48b54bd8388eb771 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 14 Oct 2021 15:45:05 +0200 Subject: [PATCH 10/12] 83635: Item request - processed message --- .../grant-deny-request-copy.component.html | 34 ++++++++++++------- .../grant-deny-request-copy.component.spec.ts | 23 +++++++++++-- src/assets/i18n/en.json5 | 4 +++ 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html index abd013f9a1..37b275d8f8 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.html @@ -1,21 +1,29 @@

{{'grant-deny-request-copy.header' | translate}}

-

-

{{'grant-deny-request-copy.intro2' | translate}}

+ diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts index 3a0817b3b0..cfefd94219 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { AuthService } from '../../core/auth/auth.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { of as observableOf } from 'rxjs'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ @@ -17,8 +17,10 @@ import { Item } from '../../core/shared/item.model'; import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy.component'; import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths'; +import { By } from '@angular/platform-browser'; +import { RemoteData } from '../../core/data/remote-data'; -describe('GrantDenyRequestCopyComponent', () => { +fdescribe('GrantDenyRequestCopyComponent', () => { let component: GrantDenyRequestCopyComponent; let fixture: ComponentFixture; @@ -120,4 +122,21 @@ describe('GrantDenyRequestCopyComponent', () => { done(); }); }); + + describe('processed message', () => { + it('should not be displayed when decisionDate is undefined', () => { + const message = fixture.debugElement.query(By.css('.processed-message')); + expect(message).toBeNull(); + }); + + it('should be displayed when decisionDate is defined', () => { + component.itemRequestRD$ = createSuccessfulRemoteDataObject$(Object.assign(new ItemRequest(), itemRequest, { + decisionDate: 'defined-date' + })); + fixture.detectChanges(); + + const message = fixture.debugElement.query(By.css('.processed-message')); + expect(message).not.toBeNull(); + }); + }); }); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index fb3c27883b..f984656e5f 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1467,10 +1467,14 @@ "grant-deny-request-copy.header": "Document copy request", + "grant-deny-request-copy.home-page": "Take me to the home page", + "grant-deny-request-copy.intro1": "If you are one of the authors of the document {{ name }}, then please use one of the options below to respond to the user's request.", "grant-deny-request-copy.intro2": "After choosing an option, you will be presented with a suggested email reply which you may edit.", + "grant-deny-request-copy.processed": "This request has already been processed. You can use the button below to get back to the home page.", + "grant-request-copy.email.message": "Dear {{ recipientName }},\nIn response to your request I have the pleasure to send you in attachment a copy of the file(s) concerning the document: \"{{ itemUrl }}\" ({{ itemName }}), of which I am an author.\n\nBest regards,\n{{ authorName }} <{{ authorEmail }}>", From 13dd4dfc05179733fd0fec24262c5bbe8dd102d9 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 14 Oct 2021 15:48:50 +0200 Subject: [PATCH 11/12] 83635: Remove fdescribe and unused imports --- .../grant-deny-request-copy.component.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts index cfefd94219..5c37a86f24 100644 --- a/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts +++ b/src/app/request-copy/grant-deny-request-copy/grant-deny-request-copy.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { AuthService } from '../../core/auth/auth.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ @@ -18,9 +18,8 @@ import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy.compone import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths'; import { By } from '@angular/platform-browser'; -import { RemoteData } from '../../core/data/remote-data'; -fdescribe('GrantDenyRequestCopyComponent', () => { +describe('GrantDenyRequestCopyComponent', () => { let component: GrantDenyRequestCopyComponent; let fixture: ComponentFixture; From 492a31dd10795489aac6547a040bc29334f96e9e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 18 Oct 2021 16:43:13 +0200 Subject: [PATCH 12/12] 83635: Remove f from fdescribe --- .../file-download-link/file-download-link.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index bf80dfcdd9..61e9ecb4aa 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -11,7 +11,7 @@ import { Item } from '../../core/shared/item.model'; import { getItemModuleRoute } from '../../item-page/item-page-routing-paths'; import { RouterLinkDirectiveStub } from '../testing/router-link-directive.stub'; -fdescribe('FileDownloadLinkComponent', () => { +describe('FileDownloadLinkComponent', () => { let component: FileDownloadLinkComponent; let fixture: ComponentFixture;