From 506883c96060c5cd9ff6f3199fca2bcc26d4ea78 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 20 Sep 2021 16:26:33 +0200 Subject: [PATCH] 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 @@
- +