mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Request-a-copy improv: Secure file section and download links
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
||||
<!-- If the user cannot download the file by auth or token, show a lock icon -->
|
||||
<span role="img" *ngIf="(canDownload$ | async) === false && (canDownloadWithToken$ | async) === false" [attr.aria-label]="'file-download-link.restricted' | translate" class="pr-1"><i class="fas fa-lock"></i></span>
|
||||
<!-- If the user can download the file by token, and NOT normally show a lock open icon -->
|
||||
<span role="img" *ngIf="(canDownloadWithToken$ | async) && (canDownload$ | async) === false" [attr.aria-label]="'file-download-link.secure-access' | translate" class="pr-1"><i class="fa-solid fa-lock-open" style="color: #26a269;"></i></span>
|
||||
<!-- Otherwise, show no icon (normal download by authorized user), public access etc. -->
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
|
||||
<ng-template #content>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
@@ -0,0 +1,343 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { getBitstreamModuleRoute } from '../../../../app-routing-paths';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||
import { ItemRequestDataService } from '../../../../core/data/item-request-data.service';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemRequest } from '../../../../core/shared/item-request.model';
|
||||
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../../shared/remote-data.utils';
|
||||
import { RouterLinkDirectiveStub } from '../../../../shared/testing/router-link-directive.stub';
|
||||
import { getItemModuleRoute } from '../../../item-page-routing-paths';
|
||||
import { ItemSecureFileDownloadLinkComponent } from './item-secure-file-download-link.component';
|
||||
|
||||
describe('FileDownloadLinkComponent', () => {
|
||||
let component: ItemSecureFileDownloadLinkComponent;
|
||||
let fixture: ComponentFixture<ItemSecureFileDownloadLinkComponent>;
|
||||
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let itemRequestDataService: ItemRequestDataService;
|
||||
let bitstream: Bitstream;
|
||||
let item: Item;
|
||||
let itemRequest: ItemRequest;
|
||||
let routeStub: any;
|
||||
|
||||
function init() {
|
||||
itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
|
||||
canDownload: observableOf(true),
|
||||
});
|
||||
bitstream = Object.assign(new Bitstream(), {
|
||||
uuid: 'bitstreamUuid',
|
||||
_links: {
|
||||
self: { href: 'obj-selflink' },
|
||||
},
|
||||
});
|
||||
item = Object.assign(new Item(), {
|
||||
uuid: 'itemUuid',
|
||||
_links: {
|
||||
self: { href: 'obj-selflink' },
|
||||
},
|
||||
});
|
||||
routeStub = {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(item),
|
||||
}),
|
||||
children: [],
|
||||
};
|
||||
|
||||
itemRequest = Object.assign(new ItemRequest(),
|
||||
{
|
||||
accessToken: 'accessToken',
|
||||
itemId: item.uuid,
|
||||
bitstreamId: bitstream.uuid,
|
||||
allfiles: false,
|
||||
requestEmail: 'user@name.org',
|
||||
requestName: 'User Name',
|
||||
requestMessage: 'I would like to request a copy',
|
||||
});
|
||||
}
|
||||
|
||||
function initTestbed() {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(), ItemSecureFileDownloadLinkComponent,
|
||||
RouterLinkDirectiveStub,
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: RouterLinkDirectiveStub },
|
||||
{ provide: ItemRequestDataService, useValue: itemRequestDataService },
|
||||
],
|
||||
}) .compileComponents();
|
||||
}
|
||||
|
||||
describe('when the user has download rights AND a valid item access token', () => {
|
||||
/**
|
||||
* We expect the normal download link to be rendered, whether or not there is a valid item request or request a copy feature
|
||||
* available, since the user already has the right to download this file
|
||||
*/
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemSecureFileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
component.item = item;
|
||||
component.itemRequest = itemRequest;
|
||||
component.enableRequestACopy = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('canDownload$ should return true', () => {
|
||||
component.canDownload$.subscribe((canDownload) => {
|
||||
expect(canDownload).toBe(true);
|
||||
});
|
||||
});
|
||||
it('canDownloadWithToken$ should return true', () => {
|
||||
component.canDownloadWithToken$.subscribe((canDownloadWithToken) => {
|
||||
expect(canDownloadWithToken).toBe(true);
|
||||
});
|
||||
});
|
||||
it('canRequestACopy$ should return true', () => {
|
||||
component.canRequestACopy$.subscribe((canRequestACopy) => {
|
||||
expect(canRequestACopy).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||
component.bitstreamPath$.subscribe((bitstreamPath) => {
|
||||
expect(bitstreamPath).toEqual({
|
||||
routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(),
|
||||
queryParams: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has download rights but no valid item access token', () => {
|
||||
/**
|
||||
* We expect the normal download link to be rendered, whether or not there is a valid item request or request a copy feature
|
||||
* available, since the user already has the right to download this file
|
||||
*/
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemSecureFileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
component.item = item;
|
||||
component.itemRequest = null;
|
||||
component.enableRequestACopy = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('canDownload$ should return true', () => {
|
||||
component.canDownload$.subscribe((canDownload) => {
|
||||
expect(canDownload).toBe(true);
|
||||
});
|
||||
});
|
||||
it('canDownloadWithToken$ should return false', () => {
|
||||
component.canDownloadWithToken$.subscribe((canDownloadWithToken) => {
|
||||
expect(canDownloadWithToken).toBe(false);
|
||||
});
|
||||
});
|
||||
it('canRequestACopy$ should return true', () => {
|
||||
component.canRequestACopy$.subscribe((canRequestACopy) => {
|
||||
expect(canRequestACopy).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||
component.bitstreamPath$.subscribe((bitstreamPath) => {
|
||||
expect(bitstreamPath).toEqual({
|
||||
routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(),
|
||||
queryParams: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has no download rights but there is a valid access token', () => {
|
||||
/**
|
||||
* We expect the download-with-token link to be rendered, since we have a valid request but no normal download rights
|
||||
*/
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
authorizationService = {
|
||||
isAuthorized: (featureId: FeatureID) => {
|
||||
if (featureId === FeatureID.CanDownload) {
|
||||
return observableOf(false);
|
||||
}
|
||||
return observableOf(true);
|
||||
},
|
||||
} as AuthorizationDataService;
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemSecureFileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
component.item = item;
|
||||
component.itemRequest = itemRequest;
|
||||
component.enableRequestACopy = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('canDownload$ should return false', () => {
|
||||
component.canDownload$.subscribe((canDownload) => {
|
||||
expect(canDownload).toBe(false);
|
||||
});
|
||||
});
|
||||
it('canDownloadWithToken$ should return true', () => {
|
||||
component.canDownloadWithToken$.subscribe((canDownloadWithToken) => {
|
||||
expect(canDownloadWithToken).toBe(true);
|
||||
});
|
||||
});
|
||||
it('canRequestACopy$ should return true', () => {
|
||||
component.canRequestACopy$.subscribe((canRequestACopy) => {
|
||||
expect(canRequestACopy).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should return the access token path based on the input bitstream', () => {
|
||||
component.bitstreamPath$.subscribe((accessTokenPath) => {
|
||||
expect(accessTokenPath).toEqual({
|
||||
routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(),
|
||||
queryParams: {
|
||||
accessToken: itemRequest.accessToken,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has no download rights but has the right to request a copy and there is no valid access token', () => {
|
||||
/**
|
||||
* We expect the request-a-copy link to be rendered instead of the normal download link or download-by-token link
|
||||
*/
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
authorizationService = {
|
||||
isAuthorized: (featureId: FeatureID) => {
|
||||
if (featureId === FeatureID.CanDownload) {
|
||||
return observableOf(false);
|
||||
}
|
||||
return observableOf(true);
|
||||
},
|
||||
} as AuthorizationDataService;
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemSecureFileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
component.bitstream = bitstream;
|
||||
component.itemRequest = null;
|
||||
component.enableRequestACopy = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('canDownload should be false', () => {
|
||||
component.canDownload$.subscribe((canDownload) => {
|
||||
expect(canDownload).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('canDownloadWithToken should be false', () => {
|
||||
component.canDownloadWithToken$.subscribe((canDownload) => {
|
||||
expect(canDownload).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('canRequestACopy should be true', () => {
|
||||
component.canRequestACopy$.subscribe((canRequestACopy) => {
|
||||
expect(canRequestACopy).toBeTrue();
|
||||
});
|
||||
});
|
||||
it('should return the bitstreamPath based a request-a-copy item + bitstream ID link', () => {
|
||||
component.bitstreamPath$.subscribe((bitstreamPath) => {
|
||||
expect(bitstreamPath).toEqual({
|
||||
routerLink: new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString(),
|
||||
queryParams: { bitstream: bitstream.uuid },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the user has no download rights and no request a copy rights and there is no valid itemRequest', () => {
|
||||
/**
|
||||
* We expect a normal download link (which would then be treated as a forbidden and redirect to the login page as per normal)
|
||||
*/
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
// This mock will return false for both canDownload and canRequestACopy checks
|
||||
authorizationService = {
|
||||
isAuthorized: (featureId: FeatureID) => {
|
||||
return observableOf(false);
|
||||
},
|
||||
} as AuthorizationDataService;
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemSecureFileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.bitstream = bitstream;
|
||||
component.item = item;
|
||||
component.itemRequest = null;
|
||||
component.enableRequestACopy = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('canDownload$ should be false', () => {
|
||||
component.canDownload$.subscribe((canDownload) => {
|
||||
expect(canDownload).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('canDownloadWithToken$ should be false', () => {
|
||||
component.canDownloadWithToken$.subscribe((canDownloadWithToken) => {
|
||||
expect(canDownloadWithToken).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('canRequestACopy$ should be false', () => {
|
||||
component.canRequestACopy$.subscribe((canRequestACopy) => {
|
||||
expect(canRequestACopy).toBeFalse();
|
||||
});
|
||||
});
|
||||
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||
component.bitstreamPath$.subscribe((bitstreamPath) => {
|
||||
expect(bitstreamPath).toEqual({
|
||||
routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(),
|
||||
queryParams: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgIf,
|
||||
NgTemplateOutlet,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
getBitstreamDownloadRoute,
|
||||
getBitstreamDownloadWithAccessTokenRoute,
|
||||
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 { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemRequest } from '../../../../core/shared/item-request.model';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../../../shared/empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-secure-file-download-link',
|
||||
templateUrl: './item-secure-file-download-link.component.html',
|
||||
styleUrls: ['./item-secure-file-download-link.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RouterLink, NgClass, NgIf, NgTemplateOutlet, AsyncPipe, TranslateModule,
|
||||
],
|
||||
})
|
||||
/**
|
||||
* Component displaying a download link
|
||||
* When the user is authenticated, a short-lived token retrieved from the REST API is added to the download link,
|
||||
* ensuring the user is authorized to download the file.
|
||||
*/
|
||||
export class ItemSecureFileDownloadLinkComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Optional bitstream instead of href and file name
|
||||
*/
|
||||
@Input() bitstream: Bitstream;
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* Additional css classes to apply to link
|
||||
*/
|
||||
@Input() cssClasses = '';
|
||||
|
||||
/**
|
||||
* A boolean representing if link is shown in same tab or in a new one.
|
||||
*/
|
||||
@Input() isBlank = false;
|
||||
|
||||
@Input() itemRequest: ItemRequest;
|
||||
|
||||
@Input() enableRequestACopy = true;
|
||||
|
||||
bitstreamPath$: Observable<{
|
||||
routerLink: string,
|
||||
queryParams: any,
|
||||
}>;
|
||||
|
||||
// authorized to download normally
|
||||
canDownload$: Observable<boolean>;
|
||||
// authorized to download with token
|
||||
canDownloadWithToken$: Observable<boolean>;
|
||||
// authorized to request a copy
|
||||
canRequestACopy$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise component observables to test access rights to a normal bitstream download, a valid token download
|
||||
* (for a given bitstream), and ability to request a copy of a bitstream.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.canDownload$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||
this.canDownloadWithToken$ = observableOf(this.itemRequest ? (this.itemRequest.allfiles !== false || this.itemRequest.bitstreamId === this.bitstream.uuid) : false);
|
||||
this.canRequestACopy$ = this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||
|
||||
this.bitstreamPath$ = observableCombineLatest([this.canDownload$, this.canDownloadWithToken$, this.canRequestACopy$]).pipe(
|
||||
map(([canDownload, canDownloadWithToken, canRequestACopy]) => this.getBitstreamPath(canDownload, canDownloadWithToken, canRequestACopy)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a path to the bitstream based on what kind of access and authorization the user has, and whether
|
||||
* they may request a copy
|
||||
*
|
||||
* @param canDownload user can download normally
|
||||
* @param canDownloadWithToken user can download using a token granted by a request approver
|
||||
* @param canRequestACopy user can request approval to access a copy
|
||||
*/
|
||||
getBitstreamPath(canDownload: boolean, canDownloadWithToken, canRequestACopy: boolean) {
|
||||
// No matter what, if the user can download with their own authZ, allow it
|
||||
if (canDownload) {
|
||||
return this.getBitstreamDownloadPath();
|
||||
}
|
||||
// Otherwise, if they access token is valid, use this
|
||||
if (canDownloadWithToken) {
|
||||
return this.getAccessByTokenBitstreamPath(this.itemRequest);
|
||||
}
|
||||
// If the user can't download, but can request a copy, show the request a copy link
|
||||
if (!canDownload && canRequestACopy && hasValue(this.item)) {
|
||||
return getBitstreamRequestACopyRoute(this.item, this.bitstream);
|
||||
}
|
||||
// By default, return the plain path
|
||||
return this.getBitstreamDownloadPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve special bitstream path which includes access token parameter
|
||||
* @param itemRequest the item request object
|
||||
*/
|
||||
getAccessByTokenBitstreamPath(itemRequest: ItemRequest) {
|
||||
return getBitstreamDownloadWithAccessTokenRoute(this.bitstream, itemRequest.accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get normal bitstream download path, with no parameters
|
||||
*/
|
||||
getBitstreamDownloadPath() {
|
||||
return {
|
||||
routerLink: getBitstreamDownloadRoute(this.bitstream),
|
||||
queryParams: {},
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<div *ngVar="(originals$ | async)?.payload as originals">
|
||||
<div *ngIf="hasValuesInBundle(originals)">
|
||||
<h3 class="h5 simple-view-element-header">{{"item.page.filesection.original.bundle" | translate}}</h3>
|
||||
<ds-pagination *ngIf="originals?.page?.length > 0"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="originalOptions"
|
||||
[collectionSize]="originals?.totalElements"
|
||||
[retainScrollPosition]="true">
|
||||
|
||||
|
||||
<div class="file-section row mb-3" *ngFor="let file of originals?.page;">
|
||||
<div class="col-3">
|
||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<dl class="row">
|
||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
||||
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
||||
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
||||
|
||||
<ng-container *ngIf="file.hasMetadata('dc.description')">
|
||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<!-- <ds-themed-file-download-link [bitstream]="file" [item]="item">-->
|
||||
<!-- {{"item.page.filesection.download" | translate}}-->
|
||||
<!-- </ds-themed-file-download-link>-->
|
||||
<ds-item-secure-file-download-link [bitstream]="file" [item]="item" [itemRequest]="itemRequest">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-item-secure-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngVar="(licenses$ | async)?.payload as licenses">
|
||||
<div *ngIf="hasValuesInBundle(licenses)">
|
||||
<h3 class="h5 simple-view-element-header">{{"item.page.filesection.license.bundle" | translate}}</h3>
|
||||
<ds-pagination *ngIf="licenses?.page?.length > 0"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="licenseOptions"
|
||||
[collectionSize]="licenses?.totalElements"
|
||||
[retainScrollPosition]="true">
|
||||
|
||||
|
||||
<div class="file-section row" *ngFor="let file of licenses?.page;">
|
||||
<div class="col-3">
|
||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<dl class="row">
|
||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
||||
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
||||
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ds-file-download-link [bitstream]="file" [item]="item">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</ds-metadata-field-wrapper>
|
@@ -0,0 +1,5 @@
|
||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
||||
dt {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
@@ -0,0 +1,96 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import {
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
} from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { APP_CONFIG } from 'src/config/app-config.interface';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component';
|
||||
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
|
||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
|
||||
import { ItemSecureFileDownloadLinkComponent } from '../file-download-link/item-secure-file-download-link.component';
|
||||
import { ItemSecureFileSectionComponent } from './item-secure-file-section.component';
|
||||
|
||||
describe('FullFileSectionComponent', () => {
|
||||
let comp: ItemSecureFileSectionComponent;
|
||||
let fixture: ComponentFixture<ItemSecureFileSectionComponent>;
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content: 'test-content-url',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
id: 'test-id',
|
||||
_links: {
|
||||
self: { href: 'test-href' },
|
||||
content: { href: 'test-content-href' },
|
||||
},
|
||||
});
|
||||
|
||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream])),
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
ItemSecureFileSectionComponent,
|
||||
VarDirective,
|
||||
FileSizePipe,
|
||||
MetadataFieldWrapperComponent,
|
||||
],
|
||||
providers: [
|
||||
provideMockStore(),
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: PaginationService, useValue: new PaginationServiceStub() },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).overrideComponent(ItemSecureFileSectionComponent, {
|
||||
remove: { imports: [PaginationComponent, MetadataFieldWrapperComponent,ItemSecureFileDownloadLinkComponent, ThemedThumbnailComponent, ThemedFileDownloadLinkComponent] },
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemSecureFileSectionComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the full file section gets loaded with bitstreams available', () => {
|
||||
it('should contain a list with bitstreams', () => {
|
||||
const fileSection = fixture.debugElement.queryAll(By.css('.file-section'));
|
||||
expect(fileSection.length).toEqual(6);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,156 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from 'src/config/app-config.interface';
|
||||
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemRequest } from '../../../../core/shared/item-request.model';
|
||||
import {
|
||||
hasValue,
|
||||
isEmpty,
|
||||
} from '../../../../shared/empty.util';
|
||||
import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component';
|
||||
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
|
||||
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
|
||||
import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component';
|
||||
import { ItemSecureFileDownloadLinkComponent } from '../file-download-link/item-secure-file-download-link.component';
|
||||
|
||||
/**
|
||||
* This component renders the file section of the item
|
||||
* inside a 'ds-metadata-field-wrapper' component.
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-secure-full-file-section',
|
||||
styleUrls: ['./item-secure-file-section.component.scss'],
|
||||
templateUrl: './item-secure-file-section.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ItemSecureFileDownloadLinkComponent,
|
||||
CommonModule,
|
||||
ThemedFileDownloadLinkComponent,
|
||||
MetadataFieldWrapperComponent,
|
||||
ThemedLoadingComponent,
|
||||
TranslateModule,
|
||||
FileSizePipe,
|
||||
VarDirective,
|
||||
PaginationComponent,
|
||||
ThemedThumbnailComponent,
|
||||
],
|
||||
})
|
||||
export class ItemSecureFileSectionComponent extends FileSectionComponent implements OnDestroy, OnInit {
|
||||
|
||||
@Input() item: Item;
|
||||
@Input() itemRequest: ItemRequest;
|
||||
|
||||
label: string;
|
||||
|
||||
originals$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
licenses$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
|
||||
originalOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'obo',
|
||||
currentPage: 1,
|
||||
pageSize: this.appConfig.item.bitstream.pageSize,
|
||||
});
|
||||
|
||||
licenseOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'lbo',
|
||||
currentPage: 1,
|
||||
pageSize: this.appConfig.item.bitstream.pageSize,
|
||||
});
|
||||
|
||||
constructor(
|
||||
bitstreamDataService: BitstreamDataService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService,
|
||||
protected paginationService: PaginationService,
|
||||
public dsoNameService: DSONameService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
) {
|
||||
super(bitstreamDataService, notificationsService, translateService, dsoNameService, appConfig);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
this.originals$ = this.paginationService.getCurrentPagination(this.originalOptions.id, this.originalOptions).pipe(
|
||||
switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
'ORIGINAL',
|
||||
{ elementsPerPage: options.pageSize, currentPage: options.currentPage },
|
||||
true,
|
||||
true,
|
||||
followLink('format'),
|
||||
followLink('thumbnail'),
|
||||
)),
|
||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(rd.errorMessage)) {
|
||||
this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.statusCode} ${rd.errorMessage}`);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.licenses$ = this.paginationService.getCurrentPagination(this.licenseOptions.id, this.licenseOptions).pipe(
|
||||
switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
'LICENSE',
|
||||
{ elementsPerPage: options.pageSize, currentPage: options.currentPage },
|
||||
true,
|
||||
true,
|
||||
followLink('format'),
|
||||
followLink('thumbnail'),
|
||||
)),
|
||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(rd.errorMessage)) {
|
||||
this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.statusCode} ${rd.errorMessage}`);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
hasValuesInBundle(bundle: PaginatedList<Bitstream>) {
|
||||
return hasValue(bundle) && !isEmpty(bundle.page);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.originalOptions.id);
|
||||
this.paginationService.clearPagination(this.licenseOptions.id);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user