Request-a-copy improv: Secure file section and download links

This commit is contained in:
Kim Shepherd
2025-02-13 14:57:15 +01:00
parent aea41d74ec
commit f3bb7327dc
8 changed files with 845 additions and 0 deletions

View File

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

View File

@@ -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: {},
});
});
});
});
});

View File

@@ -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: {},
};
}
}

View File

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

View File

@@ -0,0 +1,5 @@
@media screen and (min-width: map-get($grid-breakpoints, md)) {
dt {
text-align: right;
}
}

View File

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

View File

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