83635: Request a copy - Request page

This commit is contained in:
Yana De Pauw
2021-09-20 16:26:33 +02:00
committed by Art Lowel
parent b29b87d0f6
commit 506883c960
15 changed files with 871 additions and 36 deletions

View File

@@ -22,6 +22,9 @@ export function getBitstreamModuleRoute() {
export function getBitstreamDownloadRoute(bitstream): string { export function getBitstreamDownloadRoute(bitstream): string {
return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(); 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'; export const ADMIN_MODULE_PATH = 'admin';

View File

@@ -10,6 +10,7 @@ import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/re
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; 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_PATH = ':id/edit';
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
@@ -44,6 +45,14 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
bitstream: BitstreamPageResolver bitstream: BitstreamPageResolver
}, },
}, },
{
// Resolve angular bitstream download URLs
path: ':id/request-a-copy',
component: BitstreamRequestACopyPageComponent,
resolve: {
bitstream: BitstreamPageResolver
},
},
{ {
path: EDIT_BITSTREAM_PATH, path: EDIT_BITSTREAM_PATH,
component: EditBitstreamPageComponent, component: EditBitstreamPageComponent,

View File

@@ -14,6 +14,7 @@ export enum FeatureID {
IsCollectionAdmin = 'isCollectionAdmin', IsCollectionAdmin = 'isCollectionAdmin',
IsCommunityAdmin = 'isCommunityAdmin', IsCommunityAdmin = 'isCommunityAdmin',
CanDownload = 'canDownload', CanDownload = 'canDownload',
CanRequestACopy = 'canRequestACopy',
CanManageVersions = 'canManageVersions', CanManageVersions = 'canManageVersions',
CanManageBitstreamBundles = 'canManageBitstreamBundles', CanManageBitstreamBundles = 'canManageBitstreamBundles',
CanManageRelationships = 'canManageRelationships', CanManageRelationships = 'canManageRelationships',

View File

@@ -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<string> {
return this.halService.getEndpoint(this.linkPath);
}
getFindItemRequestEndpoint(requestID: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
filter((href: string) => isNotEmpty(href)),
map((href: string) => `${href}/${requestID}`));
}
requestACopy(itemRequest: ItemRequest): Observable<RemoteData<ItemRequest>> {
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<ItemRequest>(requestId).pipe(
getFirstCompletedRemoteData()
);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,86 @@
<div class="container">
<h3 class="mb-4">{{'bitstream-request-a-copy.header' | translate}}</h3>
<div *ngIf="canDownload$|async" class="alert alert-success">
<span>{{'bitstream-request-a-copy.alert.canDownload1' | translate}}</span>
<a [routerLink]="getBitstreamLink()">{{'bitstream-request-a-copy.alert.canDownload2'| translate}}</a>
</div>
<div>
<p>{{'bitstream-request-a-copy.intro' | translate}}</p>
<p>{{itemName}}</p>
</div>
<form [class]="'ng-invalid'" [formGroup]="requestCopyForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<div class="row mb-4">
<div class="col-12">
<label for="name">{{'bitstream-request-a-copy.name.label' | translate}}</label>
<input [className]="(name.invalid) && (name.dirty || name.touched) ? 'form-control is-invalid' :'form-control'"
type="text" id="name" formControlName="name"/>
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="invalid-feedback show-feedback">
<span *ngIf="name.errors && name.errors.required">
{{ 'bitstream-request-a-copy.name.error' | translate }}
</span>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<label
for="email">{{'bitstream-request-a-copy.email.label' | translate}}</label>
<input
[className]="(email.invalid) && (email.dirty || email.touched) ? 'form-control is-invalid' :'form-control'"
id="email" formControlName="email">
<div *ngIf="email.invalid && (email.dirty || email.touched)"
class="invalid-feedback show-feedback">
<span *ngIf="email.errors">
{{ 'bitstream-request-a-copy.email.error' | translate }}
</span>
</div>
<small class="text-muted ds-hint">{{'bitstream-request-a-copy.email.hint' |translate}}</small>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div>{{'bitstream-request-a-copy.allfiles.label' |translate}}</div>
<div class="ml-4">
<input [className]="'form-check-input'" type="radio"
id="allfiles-true" formControlName="allfiles" value="true" [checked]="true">
<label class="form-check-label"
for="allfiles-true">{{'bitstream-request-a-copy.files-all-true.label' | translate}}</label>
</div>
<div class="ml-4">
<input [className]="'form-check-input'" type="radio"
id="allfiles-false" formControlName="allfiles" value="false">
<label class="form-check-label"
for="allfiles-false">{{'bitstream-request-a-copy.files-all-false.label' | translate}}</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<label
for="message">{{'bitstream-request-a-copy.message.label' | translate}}</label>
<textarea rows="5"
[className]="'form-control'"
id="message" formControlName="message"></textarea>
</div>
</div>
</div>
</form>
<hr>
<div class="row">
<div class="col-12 text-right">
<a (click)="navigateBack()" role="button" class="btn btn-outline-secondary mr-1">
<i class="fas fa-arrow-left"></i> {{'bitstream-request-a-copy.return' | translate}}
</a>
<button
[disabled]="requestCopyForm.invalid"
class="btn btn-default btn-primary"
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
</div>
</div>
</div>

View File

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

View File

@@ -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<Bitstream>;
canDownload$: Observable<boolean>;
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<EPerson> {
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)];
}
}

View File

@@ -1,4 +1,5 @@
<a [href]="bitstreamPath" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses"> <a [href]="(bitstreamPath$| async)" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
<span *ngIf="!(canDownload$ |async)"><i class="fas fa-lock"></i></span>
<ng-container *ngTemplateOutlet="content"></ng-container> <ng-container *ngTemplateOutlet="content"></ng-container>
</a> </a>

View File

@@ -1,62 +1,132 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FileDownloadLinkComponent } from './file-download-link.component'; 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 { Bitstream } from '../../core/shared/bitstream.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getBitstreamModuleRoute } from '../../app-routing-paths'; 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', () => { describe('FileDownloadLinkComponent', () => {
let component: FileDownloadLinkComponent; let component: FileDownloadLinkComponent;
let fixture: ComponentFixture<FileDownloadLinkComponent>; let fixture: ComponentFixture<FileDownloadLinkComponent>;
let authService: AuthService; let scheduler;
let fileService: FileService; let authorizationService: AuthorizationDataService;
let bitstream: Bitstream; let bitstream: Bitstream;
function init() { function init() {
authService = jasmine.createSpyObj('authService', { authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthenticated: observableOf(true) isAuthorized: cold('-a', {a: true})
}); });
fileService = jasmine.createSpyObj('fileService', ['downloadFile']);
bitstream = Object.assign(new Bitstream(), { bitstream = Object.assign(new Bitstream(), {
uuid: 'bitstreamUuid', uuid: 'bitstreamUuid',
_links: {
self: {href: 'obj-selflink'}
}
}); });
} }
beforeEach(waitForAsync(() => { function initTestbed() {
init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [FileDownloadLinkComponent], declarations: [FileDownloadLinkComponent],
providers: [ providers: [
{ provide: AuthService, useValue: authService }, {provide: AuthorizationDataService, useValue: authorizationService},
{ provide: FileService, useValue: fileService },
] ]
}) })
.compileComponents(); .compileComponents();
})); }
beforeEach(() => {
fixture = TestBed.createComponent(FileDownloadLinkComponent);
component = fixture.componentInstance;
component.bitstream = bitstream;
fixture.detectChanges();
});
describe('init', () => { describe('init', () => {
describe('getBitstreamPath', () => { describe('getBitstreamPath', () => {
it('should set the bitstreamPath based on the input bitstream', () => { describe('when the user has download rights', () => {
expect(component.bitstreamPath).toEqual(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString()); 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());
});
}); });
}); });

View File

@@ -1,6 +1,11 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Bitstream } from '../../core/shared/bitstream.model'; 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({ @Component({
selector: 'ds-file-download-link', selector: 'ds-file-download-link',
@@ -29,13 +34,34 @@ export class FileDownloadLinkComponent implements OnInit {
*/ */
@Input() isBlank = false; @Input() isBlank = false;
bitstreamPath: string; @Input() enableRequestACopy = true;
ngOnInit() { bitstreamPath$: Observable<string>;
this.bitstreamPath = this.getBitstreamPath();
canDownload$: Observable<boolean>;
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); return getBitstreamDownloadRoute(this.bitstream);
} }
} }

View File

@@ -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 { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component';
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component'; import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
import { SearchNavbarComponent } from '../search-navbar/search-navbar.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 * Declaration needed to make sure all decorator functions are called in time
@@ -431,6 +432,7 @@ const COMPONENTS = [
GroupSearchBoxComponent, GroupSearchBoxComponent,
FileDownloadLinkComponent, FileDownloadLinkComponent,
BitstreamDownloadPageComponent, BitstreamDownloadPageComponent,
BitstreamRequestACopyPageComponent,
CollectionDropdownComponent, CollectionDropdownComponent,
ExportMetadataSelectorComponent, ExportMetadataSelectorComponent,
ConfirmationModalComponent, ConfirmationModalComponent,
@@ -512,6 +514,7 @@ const ENTRY_COMPONENTS = [
CollectionDropdownComponent, CollectionDropdownComponent,
FileDownloadLinkComponent, FileDownloadLinkComponent,
BitstreamDownloadPageComponent, BitstreamDownloadPageComponent,
BitstreamRequestACopyPageComponent,
CurationFormComponent, CurationFormComponent,
ExportMetadataSelectorComponent, ExportMetadataSelectorComponent,
ConfirmationModalComponent, ConfirmationModalComponent,

View File

@@ -10,7 +10,7 @@
</div> </div>
<div class="float-right w-15" [class.sticky-buttons]="!readMode"> <div class="float-right w-15" [class.sticky-buttons]="!readMode">
<ng-container *ngIf="readMode"> <ng-container *ngIf="readMode">
<ds-file-download-link [cssClasses]="'btn btn-link'" [isBlank]="true" [bitstream]="getBitstream()"> <ds-file-download-link [cssClasses]="'btn btn-link'" [isBlank]="true" [bitstream]="getBitstream()" [enableRequestACopy]="false">
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i> <i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
</ds-file-download-link> </ds-file-download-link>
<button class="btn btn-link" <button class="btn btn-link"

View File

@@ -588,6 +588,40 @@
"bitstream.edit.title": "Edit bitstream", "bitstream.edit.title": "Edit bitstream",
"bitstream-request-a-copy.alert.canDownload1": "You already have access to this file. If you want to download the file, click ",
"bitstream-request-a-copy.alert.canDownload2": "here",
"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.name.label": "Name *",
"bitstream-request-a-copy.name.error": "The name is required",
"bitstream-request-a-copy.email.label": "Your e-mail address *",
"bitstream-request-a-copy.email.hint": "This email address is used for sending the file.",
"bitstream-request-a-copy.email.error": "Please enter a valid email address.",
"bitstream-request-a-copy.allfiles.label": "Files",
"bitstream-request-a-copy.files-all-false.label": "Only the requested file",
"bitstream-request-a-copy.files-all-true.label": "All files (of this item) in restricted access",
"bitstream-request-a-copy.message.label": "Message",
"bitstream-request-a-copy.return": "Back",
"bitstream-request-a-copy.submit": "Request copy",
"bitstream-request-a-copy.submit.success": "The item request was submitted successfully.",
"bitstream-request-a-copy.submit.error": "Something went wrong with submitting the item request.",
"browse.comcol.by.author": "By Author", "browse.comcol.by.author": "By Author",