mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
Merge branch 'Authorization-for-downloads-of-restricted-bitstreams' into scripts-processes
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
::ng-deep {
|
:host ::ng-deep {
|
||||||
.fa-chevron-right {
|
.fa-chevron-right {
|
||||||
padding-left: $spacer/2;
|
padding-left: $spacer/2;
|
||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
|
@@ -21,9 +21,9 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<a [href]="file._links.content.href" [download]="file.name">
|
<ds-file-download-link [href]="file._links.content.href" [download]="file.name">
|
||||||
{{"item.page.filesection.download" | translate}}
|
{{"item.page.filesection.download" | translate}}
|
||||||
</a>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
||||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||||
<div class="file-section">
|
<div class="file-section">
|
||||||
<a *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
||||||
<span>{{file?.name}}</span>
|
<span>{{file?.name}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||||
</a>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
import { CommunityListService, FlatNode } from './community-list-service';
|
import { CommunityListService, FlatNode } from './community-list-service';
|
||||||
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
|
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
|
||||||
import { BehaviorSubject, Observable, } from 'rxjs';
|
import { BehaviorSubject, Observable, } from 'rxjs';
|
||||||
@@ -14,21 +16,23 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
|
|||||||
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
||||||
public loading$ = new BehaviorSubject<boolean>(false);
|
public loading$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
constructor(private communityListService: CommunityListService) {
|
constructor(private communityListService: CommunityListService,
|
||||||
|
private zone: NgZone) {
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
||||||
return this.communityList$.asObservable();
|
return this.communityList$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCommunities(expandedNodes: FlatNode[]) {
|
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) {
|
||||||
this.loading$.next(true);
|
this.loading$.next(true);
|
||||||
|
this.zone.runOutsideAngular(() => {
|
||||||
this.communityListService.loadCommunities(expandedNodes).pipe(
|
this.communityListService.loadCommunities(findOptions, expandedNodes).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
finalize(() => this.loading$.next(false)),
|
finalize(() => this.zone.run(() => this.loading$.next(false))),
|
||||||
).subscribe((flatNodes: FlatNode[]) => {
|
).subscribe((flatNodes: FlatNode[]) => {
|
||||||
this.communityList$.next(flatNodes);
|
this.zone.run(() => this.communityList$.next(flatNodes));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,21 +1,19 @@
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { inject, TestBed } from '@angular/core/testing';
|
||||||
import { TestBed, inject, async } from '@angular/core/testing';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
import { StoreMock } from '../shared/testing/store.mock';
|
import { StoreMock } from '../shared/testing/store.mock';
|
||||||
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
|
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
|
||||||
import { PageInfo } from '../core/shared/page-info.model';
|
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import {
|
|
||||||
createFailedRemoteDataObject$,
|
|
||||||
createSuccessfulRemoteDataObject$
|
|
||||||
} from '../shared/remote-data.utils';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
|
|
||||||
describe('CommunityListService', () => {
|
describe('CommunityListService', () => {
|
||||||
let store: StoreMock<AppState>;
|
let store: StoreMock<AppState>;
|
||||||
@@ -212,9 +210,11 @@ describe('CommunityListService', () => {
|
|||||||
let findTopSpy;
|
let findTopSpy;
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
|
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
|
||||||
service.getNextPageTopCommunities();
|
|
||||||
|
|
||||||
service.loadCommunities(null)
|
service.loadCommunities({
|
||||||
|
currentPage: 2,
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, null)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((value) => {
|
.subscribe((value) => {
|
||||||
flatNodeList = value;
|
flatNodeList = value;
|
||||||
@@ -240,7 +240,10 @@ describe('CommunityListService', () => {
|
|||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
|
describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
service.loadCommunities(null)
|
service.loadCommunities({
|
||||||
|
currentPage: 1,
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, null)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((value) => {
|
.subscribe((value) => {
|
||||||
flatNodeList = value;
|
flatNodeList = value;
|
||||||
@@ -270,7 +273,10 @@ describe('CommunityListService', () => {
|
|||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
expandedNodes.push(communityFlatNode);
|
expandedNodes.push(communityFlatNode);
|
||||||
});
|
});
|
||||||
service.loadCommunities(expandedNodes)
|
service.loadCommunities({
|
||||||
|
currentPage: 1,
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, expandedNodes)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((value) => {
|
.subscribe((value) => {
|
||||||
flatNodeList = value;
|
flatNodeList = value;
|
||||||
@@ -295,7 +301,10 @@ describe('CommunityListService', () => {
|
|||||||
communityFlatNode.currentCollectionPage = 1;
|
communityFlatNode.currentCollectionPage = 1;
|
||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
const expandedNodes = [communityFlatNode];
|
const expandedNodes = [communityFlatNode];
|
||||||
service.loadCommunities(expandedNodes)
|
service.loadCommunities({
|
||||||
|
currentPage: 1,
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, expandedNodes)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((value) => {
|
.subscribe((value) => {
|
||||||
flatNodeList = value;
|
flatNodeList = value;
|
||||||
@@ -317,7 +326,10 @@ describe('CommunityListService', () => {
|
|||||||
communityFlatNode.currentCollectionPage = 2;
|
communityFlatNode.currentCollectionPage = 2;
|
||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
const expandedNodes = [communityFlatNode];
|
const expandedNodes = [communityFlatNode];
|
||||||
service.loadCommunities(expandedNodes)
|
service.loadCommunities({
|
||||||
|
currentPage: 1,
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, expandedNodes)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((value) => {
|
.subscribe((value) => {
|
||||||
flatNodeList = value;
|
flatNodeList = value;
|
||||||
|
@@ -4,11 +4,12 @@ import { combineLatest as observableCombineLatest } from 'rxjs/internal/observab
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { map, flatMap } from 'rxjs/operators';
|
||||||
import { catchError, filter, map, switchMap, take } from 'rxjs/operators';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
@@ -46,8 +47,7 @@ export class ShowMoreFlatNode {
|
|||||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||||
export const combineAndFlatten = (obsList: Array<Observable<FlatNode[]>>): Observable<FlatNode[]> =>
|
export const combineAndFlatten = (obsList: Array<Observable<FlatNode[]>>): Observable<FlatNode[]> =>
|
||||||
observableCombineLatest(...obsList).pipe(
|
observableCombineLatest(...obsList).pipe(
|
||||||
map((matrix: FlatNode[][]) =>
|
map((matrix: any[][]) => [].concat(...matrix))
|
||||||
matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList]))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,6 +99,8 @@ const communityListStateSelector = (state: AppState) => state.communityList;
|
|||||||
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
||||||
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
||||||
|
|
||||||
|
export const MAX_COMCOLS_PER_PAGE = 50;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
||||||
* and connection to the store to retrieve and save the state of the community list
|
* and connection to the store to retrieve and save the state of the community list
|
||||||
@@ -107,26 +109,8 @@ const loadingNodeSelector = createSelector(communityListStateSelector, (communit
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityListService {
|
export class CommunityListService {
|
||||||
|
|
||||||
// page-limited list of top-level communities
|
|
||||||
payloads$: Array<Observable<PaginatedList<Community>>>;
|
|
||||||
|
|
||||||
topCommunitiesConfig: PaginationComponentOptions;
|
|
||||||
topCommunitiesSortConfig: SortOptions;
|
|
||||||
|
|
||||||
maxSubCommunitiesPerPage: number;
|
|
||||||
maxCollectionsPerPage: number;
|
|
||||||
|
|
||||||
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
|
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
|
||||||
private store: Store<any>) {
|
private store: Store<any>) {
|
||||||
this.topCommunitiesConfig = new PaginationComponentOptions();
|
|
||||||
this.topCommunitiesConfig.id = 'top-level-pagination';
|
|
||||||
this.topCommunitiesConfig.pageSize = 10;
|
|
||||||
this.topCommunitiesConfig.currentPage = 1;
|
|
||||||
this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
|
||||||
this.initTopCommunityList();
|
|
||||||
|
|
||||||
this.maxSubCommunitiesPerPage = 3;
|
|
||||||
this.maxCollectionsPerPage = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
|
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
|
||||||
@@ -141,57 +125,46 @@ export class CommunityListService {
|
|||||||
return this.store.select(loadingNodeSelector);
|
return this.store.select(loadingNodeSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Increases the payload so it contains the next page of top level communities
|
|
||||||
*/
|
|
||||||
getNextPageTopCommunities(): void {
|
|
||||||
this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1;
|
|
||||||
this.payloads$ = [...this.payloads$, this.communityDataService.findTop({
|
|
||||||
currentPage: this.topCommunitiesConfig.currentPage,
|
|
||||||
elementsPerPage: this.topCommunitiesConfig.pageSize,
|
|
||||||
sort: {
|
|
||||||
field: this.topCommunitiesSortConfig.field,
|
|
||||||
direction: this.topCommunitiesSortConfig.direction
|
|
||||||
}
|
|
||||||
}).pipe(
|
|
||||||
take(1),
|
|
||||||
map((results) => results.payload),
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all top communities, limited by page, and transforms this in a list of flatNodes.
|
* Gets all top communities, limited by page, and transforms this in a list of flatNodes.
|
||||||
* @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need
|
* @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need
|
||||||
* not be added to the list
|
* not be added to the list
|
||||||
*/
|
*/
|
||||||
loadCommunities(expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||||
const res = this.payloads$.map((payload) => {
|
const currentPage = findOptions.currentPage;
|
||||||
return payload.pipe(
|
const topCommunities = [];
|
||||||
take(1),
|
for (let i = 1; i <= currentPage; i++) {
|
||||||
switchMap((result: PaginatedList<Community>) => {
|
const pagination: FindListOptions = Object.assign({}, findOptions, { currentPage: i });
|
||||||
return this.transformListOfCommunities(result, 0, null, expandedNodes);
|
topCommunities.push(this.getTopCommunities(pagination));
|
||||||
}),
|
}
|
||||||
catchError(() => observableOf([])),
|
const topComs$ = observableCombineLatest(...topCommunities).pipe(
|
||||||
|
map((coms: Array<PaginatedList<Community>>) => {
|
||||||
|
const newPages: Community[][] = coms.map((unit: PaginatedList<Community>) => unit.page);
|
||||||
|
const newPage: Community[] = [].concat(...newPages);
|
||||||
|
let newPageInfo = new PageInfo();
|
||||||
|
if (coms && coms.length > 0) {
|
||||||
|
newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage })
|
||||||
|
}
|
||||||
|
return new PaginatedList(newPageInfo, newPage);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
return topComs$.pipe(flatMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)));
|
||||||
return combineAndFlatten(res);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts the initial top level communities in a list to be called upon
|
* Puts the initial top level communities in a list to be called upon
|
||||||
*/
|
*/
|
||||||
private initTopCommunityList(): void {
|
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
|
||||||
this.payloads$ = [this.communityDataService.findTop({
|
return this.communityDataService.findTop({
|
||||||
currentPage: this.topCommunitiesConfig.currentPage,
|
currentPage: options.currentPage,
|
||||||
elementsPerPage: this.topCommunitiesConfig.pageSize,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
sort: {
|
sort: {
|
||||||
field: this.topCommunitiesSortConfig.field,
|
field: options.sort.field,
|
||||||
direction: this.topCommunitiesSortConfig.direction
|
direction: options.sort.direction
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
take(1),
|
|
||||||
map((results) => results.payload),
|
map((results) => results.payload),
|
||||||
)];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,16 +179,15 @@ export class CommunityListService {
|
|||||||
parent: FlatNode,
|
parent: FlatNode,
|
||||||
expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||||
if (isNotEmpty(listOfPaginatedCommunities.page)) {
|
if (isNotEmpty(listOfPaginatedCommunities.page)) {
|
||||||
let currentPage = this.topCommunitiesConfig.currentPage;
|
let currentPage = listOfPaginatedCommunities.currentPage;
|
||||||
if (isNotEmpty(parent)) {
|
if (isNotEmpty(parent)) {
|
||||||
currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
|
currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
|
||||||
}
|
}
|
||||||
const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage));
|
|
||||||
let obsList = listOfPaginatedCommunities.page
|
let obsList = listOfPaginatedCommunities.page
|
||||||
.map((community: Community) => {
|
.map((community: Community) => {
|
||||||
return this.transformCommunity(community, level, parent, expandedNodes)
|
return this.transformCommunity(community, level, parent, expandedNodes)
|
||||||
});
|
});
|
||||||
if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) {
|
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
|
||||||
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +224,12 @@ export class CommunityListService {
|
|||||||
let subcoms = [];
|
let subcoms = [];
|
||||||
for (let i = 1; i <= currentCommunityPage; i++) {
|
for (let i = 1; i <= currentCommunityPage; i++) {
|
||||||
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
||||||
elementsPerPage: this.maxSubCommunitiesPerPage,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
flatMap((rd: RemoteData<PaginatedList<Community>>) =>
|
||||||
switchMap((rd: RemoteData<PaginatedList<Community>>) =>
|
|
||||||
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
|
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -271,16 +242,15 @@ export class CommunityListService {
|
|||||||
let collections = [];
|
let collections = [];
|
||||||
for (let i = 1; i <= currentCollectionPage; i++) {
|
for (let i = 1; i <= currentCollectionPage; i++) {
|
||||||
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
||||||
elementsPerPage: this.maxCollectionsPerPage,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
|
||||||
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
||||||
let nodes = rd.payload.page
|
let nodes = rd.payload.page
|
||||||
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
||||||
if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) {
|
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
||||||
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
@@ -305,21 +275,18 @@ export class CommunityListService {
|
|||||||
let hasColls$: Observable<boolean>;
|
let hasColls$: Observable<boolean>;
|
||||||
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
|
||||||
map((results) => results.payload.totalElements > 0),
|
map((results) => results.payload.totalElements > 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
|
||||||
map((results) => results.payload.totalElements > 0),
|
map((results) => results.payload.totalElements > 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let hasChildren$: Observable<boolean>;
|
let hasChildren$: Observable<boolean>;
|
||||||
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
|
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
|
||||||
take(1),
|
|
||||||
map(([hasSubcoms, hasColls]: [boolean, boolean]) => {
|
map(([hasSubcoms, hasColls]: [boolean, boolean]) => {
|
||||||
if (hasSubcoms || hasColls) {
|
if (hasSubcoms || hasColls) {
|
||||||
return true;
|
return true;
|
||||||
|
@@ -114,15 +114,9 @@ describe('CommunityListComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
communityListServiceStub = {
|
communityListServiceStub = {
|
||||||
topPageSize: 2,
|
pageSize: 2,
|
||||||
topCurrentPage: 1,
|
|
||||||
collectionPageSize: 2,
|
|
||||||
subcommunityPageSize: 2,
|
|
||||||
expandedNodes: [],
|
expandedNodes: [],
|
||||||
loadingNode: null,
|
loadingNode: null,
|
||||||
getNextPageTopCommunities() {
|
|
||||||
this.topCurrentPage++;
|
|
||||||
},
|
|
||||||
getLoadingNodeFromStore() {
|
getLoadingNodeFromStore() {
|
||||||
return observableOf(this.loadingNode);
|
return observableOf(this.loadingNode);
|
||||||
},
|
},
|
||||||
@@ -133,12 +127,12 @@ describe('CommunityListComponent', () => {
|
|||||||
this.expandedNodes = expandedNodes;
|
this.expandedNodes = expandedNodes;
|
||||||
this.loadingNode = loadingNode;
|
this.loadingNode = loadingNode;
|
||||||
},
|
},
|
||||||
loadCommunities(expandedNodes) {
|
loadCommunities(options, expandedNodes) {
|
||||||
let flatnodes;
|
let flatnodes;
|
||||||
let showMoreTopComNode = false;
|
let showMoreTopComNode = false;
|
||||||
flatnodes = [...mockTopFlatnodesUnexpanded];
|
flatnodes = [...mockTopFlatnodesUnexpanded];
|
||||||
const currentPage = this.topCurrentPage;
|
const currentPage = options.currentPage;
|
||||||
const elementsPerPage = this.topPageSize;
|
const elementsPerPage = this.pageSize;
|
||||||
let endPageIndex = (currentPage * elementsPerPage);
|
let endPageIndex = (currentPage * elementsPerPage);
|
||||||
if (endPageIndex >= flatnodes.length) {
|
if (endPageIndex >= flatnodes.length) {
|
||||||
endPageIndex = flatnodes.length;
|
endPageIndex = flatnodes.length;
|
||||||
@@ -171,14 +165,14 @@ describe('CommunityListComponent', () => {
|
|||||||
collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)];
|
collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)];
|
||||||
});
|
});
|
||||||
if (isNotEmpty(subComFlatnodes)) {
|
if (isNotEmpty(subComFlatnodes)) {
|
||||||
const endSubComIndex = this.subcommunityPageSize * expandedParent.currentCommunityPage;
|
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
|
||||||
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
||||||
if (subComFlatnodes.length > endSubComIndex) {
|
if (subComFlatnodes.length > endSubComIndex) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNotEmpty(collFlatnodes)) {
|
if (isNotEmpty(collFlatnodes)) {
|
||||||
const endColIndex = this.collectionPageSize * expandedParent.currentCollectionPage;
|
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
|
||||||
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
||||||
if (collFlatnodes.length > endColIndex) {
|
if (collFlatnodes.length > endColIndex) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
import { CommunityListService, FlatNode } from '../community-list-service';
|
import { CommunityListService, FlatNode } from '../community-list-service';
|
||||||
import { CommunityListDatasource } from '../community-list-datasource';
|
import { CommunityListDatasource } from '../community-list-datasource';
|
||||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
@@ -27,17 +29,24 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
dataSource: CommunityListDatasource;
|
dataSource: CommunityListDatasource;
|
||||||
|
|
||||||
constructor(private communityListService: CommunityListService) {
|
paginationConfig: FindListOptions;
|
||||||
|
|
||||||
|
constructor(private communityListService: CommunityListService,
|
||||||
|
private zone: NgZone) {
|
||||||
|
this.paginationConfig = new FindListOptions();
|
||||||
|
this.paginationConfig.elementsPerPage = 2;
|
||||||
|
this.paginationConfig.currentPage = 1;
|
||||||
|
this.paginationConfig.sort = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dataSource = new CommunityListDatasource(this.communityListService);
|
this.dataSource = new CommunityListDatasource(this.communityListService, this.zone);
|
||||||
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
||||||
this.loadingNode = result;
|
this.loadingNode = result;
|
||||||
});
|
});
|
||||||
this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => {
|
this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => {
|
||||||
this.expandedNodes = [...result];
|
this.expandedNodes = [...result];
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +83,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
node.currentCommunityPage = 1;
|
node.currentCommunityPage = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,10 +103,10 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||||
parentNodeInExpandedNodes.currentCommunityPage++;
|
parentNodeInExpandedNodes.currentCommunityPage++;
|
||||||
}
|
}
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
} else {
|
} else {
|
||||||
this.communityListService.getNextPageTopCommunities();
|
this.paginationConfig.currentPage++;
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models';
|
import {
|
||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
AuthGetRequest,
|
||||||
|
AuthPostRequest,
|
||||||
|
GetRequest,
|
||||||
|
PostRequest,
|
||||||
|
RestRequest,
|
||||||
|
TokenPostRequest
|
||||||
|
} from '../data/request.models';
|
||||||
|
import { AuthStatusResponse, ErrorResponse, TokenResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
@@ -15,6 +21,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
protected linkName = 'authn';
|
protected linkName = 'authn';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
||||||
|
|
||||||
constructor(protected halService: HALEndpointService,
|
constructor(protected halService: HALEndpointService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -67,4 +74,19 @@ export class AuthRequestService {
|
|||||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||||
distinctUntilChanged());
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST request to retrieve a short-lived token which provides download access of restricted files
|
||||||
|
*/
|
||||||
|
public getShortlivedToken(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(`${this.linkName}/${this.shortlivedtokensEndpoint}`).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new TokenPostRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||||
|
switchMap((request: PostRequest) => this.requestService.getByUUID(request.uuid)),
|
||||||
|
getResponseFromEntry(),
|
||||||
|
map((response: TokenResponse) => response.token)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
import { async, inject, TestBed } from '@angular/core/testing';
|
import { async, inject, TestBed } from '@angular/core/testing';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { authReducer, AuthState } from './auth.reducer';
|
import { authReducer, AuthState } from './auth.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
||||||
import { RouterStub } from '../../shared/testing/router.stub';
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub';
|
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub';
|
||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
@@ -49,6 +46,7 @@ describe('AuthService test', () => {
|
|||||||
let storage: CookieService;
|
let storage: CookieService;
|
||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
let authenticatedState;
|
let authenticatedState;
|
||||||
|
let unAuthenticatedState;
|
||||||
let linkService;
|
let linkService;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -67,6 +65,13 @@ describe('AuthService test', () => {
|
|||||||
authToken: token,
|
authToken: token,
|
||||||
user: EPersonMock
|
user: EPersonMock
|
||||||
};
|
};
|
||||||
|
unAuthenticatedState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: undefined,
|
||||||
|
user: undefined
|
||||||
|
};
|
||||||
authRequest = new AuthRequestServiceStub();
|
authRequest = new AuthRequestServiceStub();
|
||||||
routeStub = new ActivatedRouteStub();
|
routeStub = new ActivatedRouteStub();
|
||||||
linkService = {
|
linkService = {
|
||||||
@@ -214,6 +219,12 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return the shortlived token when user is logged in', () => {
|
||||||
|
authService.getShortlivedToken().subscribe((shortlivedToken: string) => {
|
||||||
|
expect(shortlivedToken).toEqual(authRequest.mockShortLivedToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return token object when it is valid', () => {
|
it('should return token object when it is valid', () => {
|
||||||
authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => {
|
authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => {
|
||||||
expect(tokenState).toBe(token);
|
expect(tokenState).toBe(token);
|
||||||
@@ -448,4 +459,44 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when user is not logged in', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({ authReducer }, {
|
||||||
|
runtimeChecks: {
|
||||||
|
strictStateImmutability: false,
|
||||||
|
strictActionImmutability: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthRequestService, useValue: authRequest },
|
||||||
|
{ provide: REQUEST, useValue: {} },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: linkService },
|
||||||
|
CookieService,
|
||||||
|
AuthService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = unAuthenticatedState;
|
||||||
|
});
|
||||||
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should return null for the shortlived token', () => {
|
||||||
|
authService.getShortlivedToken().subscribe((shortlivedToken: string) => {
|
||||||
|
expect(shortlivedToken).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -546,4 +546,14 @@ export class AuthService {
|
|||||||
return this.getImpersonateID() === epersonId;
|
return this.getImpersonateID() === epersonId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a short-lived token for appending to download urls of restricted files
|
||||||
|
* Returns null if the user isn't authenticated
|
||||||
|
*/
|
||||||
|
getShortlivedToken(): Observable<string> {
|
||||||
|
return this.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
45
src/app/core/auth/token-response-parsing.service.spec.ts
Normal file
45
src/app/core/auth/token-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { TokenResponseParsingService } from './token-response-parsing.service';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { TokenResponse } from '../cache/response.models';
|
||||||
|
|
||||||
|
describe('TokenResponseParsingService', () => {
|
||||||
|
let service: TokenResponseParsingService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new TokenResponseParsingService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
it('should return a TokenResponse containing the token', () => {
|
||||||
|
const data = {
|
||||||
|
payload: {
|
||||||
|
token: 'valid-token'
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
const expected = new TokenResponse(data.payload.token, true, 200, 'OK');
|
||||||
|
expect(service.parse(undefined, data)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty TokenResponse when payload doesn\'t contain a token', () => {
|
||||||
|
const data = {
|
||||||
|
payload: {},
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
const expected = new TokenResponse(null, false, 200, 'OK');
|
||||||
|
expect(service.parse(undefined, data)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error TokenResponse when the response failed', () => {
|
||||||
|
const data = {
|
||||||
|
payload: {},
|
||||||
|
statusCode: 400,
|
||||||
|
statusText: 'BAD REQUEST'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
const expected = new TokenResponse(null, false, 400, 'BAD REQUEST');
|
||||||
|
expect(service.parse(undefined, data)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
src/app/core/auth/token-response-parsing.service.ts
Normal file
23
src/app/core/auth/token-response-parsing.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ResponseParsingService } from '../data/parsing.service';
|
||||||
|
import { RestRequest } from '../data/request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { RestResponse, TokenResponse } from '../cache/response.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a token string
|
||||||
|
* wrapped in a TokenResponse
|
||||||
|
*/
|
||||||
|
export class TokenResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload.token) && (data.statusCode === 200)) {
|
||||||
|
return new TokenResponse(data.payload.token, true, data.statusCode, data.statusText);
|
||||||
|
} else {
|
||||||
|
return new TokenResponse(null, false, data.statusCode, data.statusText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -150,12 +150,7 @@ export class RemoteDataBuildService {
|
|||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((response: DSOSuccessResponse) => {
|
map((response: DSOSuccessResponse) => {
|
||||||
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
||||||
const resPageInfo = (response as DSOSuccessResponse).pageInfo;
|
return (response as DSOSuccessResponse).pageInfo;
|
||||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
|
||||||
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
|
||||||
} else {
|
|
||||||
return resPageInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
14
src/app/core/cache/response.models.ts
vendored
14
src/app/core/cache/response.models.ts
vendored
@@ -166,6 +166,20 @@ export class AuthStatusResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A REST Response containing a token
|
||||||
|
*/
|
||||||
|
export class TokenResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public token: string,
|
||||||
|
public isSuccessful: boolean,
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string
|
||||||
|
) {
|
||||||
|
super(isSuccessful, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntegrationSuccessResponse extends RestResponse {
|
export class IntegrationSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public dataDefinition: PaginatedList<IntegrationModel>,
|
public dataDefinition: PaginatedList<IntegrationModel>,
|
||||||
|
@@ -146,6 +146,7 @@ import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
|||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||||
|
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -265,6 +266,7 @@ const PROVIDERS = [
|
|||||||
ProcessFilesResponseParsingService,
|
ProcessFilesResponseParsingService,
|
||||||
MetadataSchemaDataService,
|
MetadataSchemaDataService,
|
||||||
MetadataFieldDataService,
|
MetadataFieldDataService,
|
||||||
|
TokenResponseParsingService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -19,6 +19,7 @@ import { TaskResponseParsingService } from '../tasks/task-response-parsing.servi
|
|||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
||||||
|
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -266,6 +267,15 @@ export class AuthGetRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A POST request for retrieving a token
|
||||||
|
*/
|
||||||
|
export class TokenPostRequest extends PostRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return TokenResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntegrationRequest extends GetRequest {
|
export class IntegrationRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string) {
|
constructor(uuid: string, href: string) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
|
|
||||||
import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides utility methods to save files on the client-side.
|
* Provides utility methods to save files on the client-side.
|
||||||
@@ -12,21 +12,19 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileService {
|
export class FileService {
|
||||||
constructor(
|
constructor(
|
||||||
private restService: DSpaceRESTv2Service
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
private authService: AuthService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a HTTP Get request to download a file
|
* Combines an URL with a short-lived token and sets the current URL to the newly created one
|
||||||
*
|
*
|
||||||
* @param url
|
* @param url
|
||||||
* file url
|
* file url
|
||||||
*/
|
*/
|
||||||
downloadFile(url: string) {
|
downloadFile(url: string) {
|
||||||
const headers = new HttpHeaders();
|
this.authService.getShortlivedToken().pipe(take(1)).subscribe((token) => {
|
||||||
const options: HttpOptions = Object.create({headers, responseType: 'blob'});
|
this._window.nativeWindow.location.href = hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url;
|
||||||
return this.restService.request(RestRequestMethod.GET, url, null, options)
|
|
||||||
.subscribe((data) => {
|
|
||||||
saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<a *ngIf="!(isAuthenticated$ | async)" [href]="href" [download]="download"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||||
|
<a *ngIf="(isAuthenticated$ | async)" [href]="href" [download]="download" (click)="downloadFile()"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||||
|
|
||||||
|
<ng-template #content>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</ng-template>
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { FileDownloadLinkComponent } from './file-download-link.component';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { FileService } from '../../core/shared/file.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
describe('FileDownloadLinkComponent', () => {
|
||||||
|
let component: FileDownloadLinkComponent;
|
||||||
|
let fixture: ComponentFixture<FileDownloadLinkComponent>;
|
||||||
|
|
||||||
|
let authService: AuthService;
|
||||||
|
let fileService: FileService;
|
||||||
|
let href: string;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true)
|
||||||
|
});
|
||||||
|
fileService = jasmine.createSpyObj('fileService', ['downloadFile']);
|
||||||
|
href = 'test-download-file-link';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ FileDownloadLinkComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: FileService, useValue: fileService }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.href = href;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('downloadFile', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = component.downloadFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call fileService.downloadFile with the provided href', () => {
|
||||||
|
expect(fileService.downloadFile).toHaveBeenCalledWith(href);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,48 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { FileService } from '../../core/shared/file.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-file-download-link',
|
||||||
|
templateUrl: './file-download-link.component.html',
|
||||||
|
styleUrls: ['./file-download-link.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* 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 FileDownloadLinkComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Href to link to
|
||||||
|
*/
|
||||||
|
@Input() href: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional file name for the download
|
||||||
|
*/
|
||||||
|
@Input() download: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current user is authenticated
|
||||||
|
*/
|
||||||
|
isAuthenticated$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(private fileService: FileService,
|
||||||
|
private authService: AuthService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isAuthenticated$ = this.authService.isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a download of the file
|
||||||
|
* Return false to ensure the original href is displayed when the user hovers over the link
|
||||||
|
*/
|
||||||
|
downloadFile(): boolean {
|
||||||
|
this.fileService.downloadFile(this.href);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -204,6 +204,7 @@ import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/reso
|
|||||||
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
||||||
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
||||||
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
||||||
|
import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component';
|
||||||
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
|
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
@@ -390,6 +391,7 @@ const COMPONENTS = [
|
|||||||
EpersonGroupListComponent,
|
EpersonGroupListComponent,
|
||||||
EpersonSearchBoxComponent,
|
EpersonSearchBoxComponent,
|
||||||
GroupSearchBoxComponent,
|
GroupSearchBoxComponent,
|
||||||
|
FileDownloadLinkComponent,
|
||||||
CollectionDropdownComponent
|
CollectionDropdownComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -463,7 +465,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
ClaimedTaskActionsApproveComponent,
|
ClaimedTaskActionsApproveComponent,
|
||||||
ClaimedTaskActionsRejectComponent,
|
ClaimedTaskActionsRejectComponent,
|
||||||
ClaimedTaskActionsReturnToPoolComponent,
|
ClaimedTaskActionsReturnToPoolComponent,
|
||||||
ClaimedTaskActionsEditMetadataComponent
|
ClaimedTaskActionsEditMetadataComponent,
|
||||||
|
FileDownloadLinkComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||||
|
@@ -9,6 +9,7 @@ import { EPersonMock } from './eperson.mock';
|
|||||||
export class AuthRequestServiceStub {
|
export class AuthRequestServiceStub {
|
||||||
protected mockUser: EPerson = EPersonMock;
|
protected mockUser: EPerson = EPersonMock;
|
||||||
protected mockTokenInfo = new AuthTokenInfo('test_token');
|
protected mockTokenInfo = new AuthTokenInfo('test_token');
|
||||||
|
protected mockShortLivedToken = 'test-shortlived-token';
|
||||||
|
|
||||||
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
|
||||||
const authStatusStub: AuthStatus = new AuthStatus();
|
const authStatusStub: AuthStatus = new AuthStatus();
|
||||||
@@ -82,4 +83,8 @@ export class AuthRequestServiceStub {
|
|||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getShortlivedToken() {
|
||||||
|
return observableOf(this.mockShortLivedToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user