From dd3445b84687fbf42fe822f2353af49d3afbd936 Mon Sep 17 00:00:00 2001 From: Florent POITTEVIN <poittevin.florent@gmail.com> Date: Tue, 23 Jun 2020 15:58:30 +0200 Subject: [PATCH] fix: DLCM-1489 in archive detail avoid download when not possible and check rigth to disable button accordingly (todo on sarch list table) --- .../data-file/deposit-data-file.state.ts | 2 +- .../features/deposit/stores/deposit.state.ts | 2 +- .../home-archive-detail.presentational.html | 74 ++++++++++-------- .../home-archive-detail.presentational.scss | 4 + .../home-archive-detail.presentational.ts | 77 ++++++++++++++++++- src/app/features/home/stores/home.action.ts | 2 +- src/app/features/home/stores/home.state.ts | 8 +- src/app/shared/services/download.service.ts | 63 ++++++++------- src/app/shared/services/security.service.ts | 11 +++ src/assets/i18n/de.json | 4 + src/assets/i18n/en.json | 4 + src/assets/i18n/fr.json | 4 + 12 files changed, 186 insertions(+), 69 deletions(-) diff --git a/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts b/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts index 52ccb771a..30aa97068 100644 --- a/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts +++ b/src/app/features/deposit/stores/data-file/deposit-data-file.state.ts @@ -215,7 +215,7 @@ export class DepositDataFileState extends CompositionState<DepositDataFileStateM } @Action(DepositDataFileAction.Download) - download(ctx: StateContext<DepositDataFileStateModel>, action: DepositDataFileAction.Download): Observable<void> { + download(ctx: StateContext<DepositDataFileStateModel>, action: DepositDataFileAction.Download): Observable<boolean> { const url = `${this._urlResource}/${action.parentId}/${ApiResourceNameEnum.DATAFILE}/${action.dataFile.resId}/${ApiActionEnum.DL}`; return this.downloadService.download(url, action.dataFile.fileName, action.dataFile.fileSize); } diff --git a/src/app/features/deposit/stores/deposit.state.ts b/src/app/features/deposit/stores/deposit.state.ts index ac4b7b6b7..e2277c356 100644 --- a/src/app/features/deposit/stores/deposit.state.ts +++ b/src/app/features/deposit/stores/deposit.state.ts @@ -845,7 +845,7 @@ export class DepositState extends ResourceState<DepositStateModel, Deposit> { } @Action(DepositAction.Download) - download(ctx: StateContext<DepositStateModel>, action: DepositAction.Download): Observable<void | Blob> { + download(ctx: StateContext<DepositStateModel>, action: DepositAction.Download): Observable<boolean> { let fileName = this._DEPOSIT_FILE_DOWNLOAD_PREFIX + action.id; let url = `${this._urlResource}/${action.id}/${ApiActionEnum.DL}`; if (isNotNullNorUndefined(action.fullFolderName)) { diff --git a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html index c6e8466f0..cb11d9655 100644 --- a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html +++ b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.html @@ -81,40 +81,48 @@ </div> </dl> -<button *ngIf="isLoggedIn || isPublicMetadata()" - mat-flat-button - [dlcmButtonSpinner]="isLoadingPrepareDownload" - color="primary" - solidifyShortCuts - (onEnter)="download(archiveMetadata)" - (click)="download(archiveMetadata)" -> - <dlcm-shared-icon [iconName]="iconNameEnum.download"></dlcm-shared-icon> - {{'homePage.archive.detail.download' | translate}} -</button> +<div class="button-wrapper"> + <div [matTooltip]="getTooltipDownload(!(isDownloadAuthorized() | solidifyAsync)) | translate"> + <button *ngIf="isLoggedIn" + mat-flat-button + [dlcmButtonSpinner]="isLoadingPrepareDownload" + color="primary" + solidifyShortCuts + [disabled]="!(isDownloadAuthorized() | solidifyAsync)" + (onEnter)="download(archiveMetadata)" + (click)="download(archiveMetadata)" + > + <dlcm-shared-icon [iconName]="iconNameEnum.download"></dlcm-shared-icon> + {{'homePage.archive.detail.download' | translate}} + </button> + </div> -<button *ngIf="isLoggedIn" - mat-flat-button - color="primary" - class="add-to-cart-button" - solidifyShortCuts - (onEnter)="addToCart(archiveMetadata)" - (click)="addToCart(archiveMetadata)" + <div [matTooltip]="getTooltipDownload(!(isDownloadAuthorized() | solidifyAsync)) | translate"> + <button *ngIf="isLoggedIn" + mat-flat-button + class="add-to-cart-button" + color="primary" + solidifyShortCuts + [disabled]="!(isDownloadAuthorized() | solidifyAsync)" + (onEnter)="addToCart(archiveMetadata)" + (click)="addToCart(archiveMetadata)" -> - <dlcm-shared-icon [iconName]="iconNameEnum.addToCart"></dlcm-shared-icon> - {{'homePage.archive.detail.button.addToCart' | translate}} -</button> + > + <dlcm-shared-icon [iconName]="iconNameEnum.addToCart"></dlcm-shared-icon> + {{'homePage.archive.detail.button.addToCart' | translate}} + </button> + </div> -<button *ngIf="isLoggedIn && isRestrictedOrClosedAccessLevel()" - mat-flat-button - color="primary" - class="add-to-cart-button" - solidifyShortCuts - (onEnter)="askAccess(archiveMetadata)" - (click)="askAccess(archiveMetadata)" + <button *ngIf="isAskAccessAvailable() | solidifyAsync" + mat-flat-button + color="primary" + class="add-to-cart-button" + solidifyShortCuts + (onEnter)="askAccess(archiveMetadata)" + (click)="askAccess(archiveMetadata)" -> - <dlcm-shared-icon [iconName]="iconNameEnum.sendRequest"></dlcm-shared-icon> - {{'homePage.archive.detail.button.askAccess' | translate}} -</button> + > + <dlcm-shared-icon [iconName]="iconNameEnum.sendRequest"></dlcm-shared-icon> + {{'homePage.archive.detail.button.askAccess' | translate}} + </button> +</div> diff --git a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss index 70b258d69..f4bf9ba41 100644 --- a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss +++ b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.scss @@ -80,4 +80,8 @@ $padding-bottom-desktop: 20px; .add-to-cart-button { margin-left: 10px; } + + .button-wrapper { + display: flex; + } } diff --git a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts index 5f0b178ad..c833212e1 100644 --- a/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts +++ b/src/app/features/home/components/presentationals/home-archive-form/home-archive-detail.presentational.ts @@ -11,11 +11,14 @@ import {ArchiveMetadata} from "@app/shared/models/business/archive-metadata.mode import {MetadataUtil} from "@app/shared/utils/metadata.util"; import {Enums} from "@enums"; import {GetShortDoiWrapper} from "@shared/components/presentationals/shared-doi-menu/shared-doi-menu.presentational"; +import {ObservableOrPromiseOrValue} from "@shared/models/extra-button-toolbar.model"; import {FileSizePipe} from "@shared/pipes/file-size.pipe"; +import {SecurityService} from "@shared/services/security.service"; import { BehaviorSubject, Observable, } from "rxjs"; +import {map} from "rxjs/operators"; import { DateUtil, EnumUtil, @@ -69,6 +72,14 @@ export class HomeArchiveDetailPresentational extends SharedAbstractPresentationa return TypeInfoEnum; } + constructor(protected readonly _securityService: SecurityService) { + super(); + } + + get orgUnitResId(): string { + return MetadataUtil.getOrganizationalUnitResId(this.archiveMetadata.metadata); + } + ngOnInit(): void { const metadata = this.archiveMetadata.metadata; this.fileSizePipe = new FileSizePipe(); @@ -183,12 +194,72 @@ export class HomeArchiveDetailPresentational extends SharedAbstractPresentationa return MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata) === Enums.Deposit.AccessEnum.PUBLIC; } - isRestrictedOrClosedAccessLevel(): boolean { + isRestrictedMetadata(): boolean { + if (isNullOrUndefined(this.archiveMetadata)) { + return false; + } + return MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata) === Enums.Deposit.AccessEnum.RESTRICTED; + } + + isClosedMetadata(): boolean { if (isNullOrUndefined(this.archiveMetadata)) { return false; } - const accessLevel = MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata); - return accessLevel === Enums.Deposit.AccessEnum.RESTRICTED || accessLevel === Enums.Deposit.AccessEnum.CLOSED; + return MetadataUtil.getAccessLevel(this.searchScope, this.archiveMetadata.metadata) === Enums.Deposit.AccessEnum.CLOSED; + } + + isDownloadAuthorized(): ObservableOrPromiseOrValue<boolean> { + if (this.isPublicMetadata()) { + return true; + } + if (!this.isLoggedIn) { + return false; + } + + if (this._securityService.isRootOrAdmin()) { + return true; + } + + if (this.isRestrictedMetadata()) { + return this._securityService.isMemberOfOrgUnit(this.orgUnitResId); + } + + if (this.isClosedMetadata()) { + return this._securityService.isStewardOfOrgUnit(this.orgUnitResId); + } + } + + isAskAccessAvailable(): ObservableOrPromiseOrValue<boolean> { + if (!this.isLoggedIn) { + return false; + } + if (this.isPublicMetadata()) { + return false; + } + const isMemberOfOrgUnit = this._securityService.isMemberOfOrgUnit(this.orgUnitResId); + if (!isMemberOfOrgUnit) { + return true; + } + if (this.isRestrictedMetadata()) { + // FOR RESTRICTED : NEED TO BE MEMBER + return true; + } + if (this.isClosedMetadata()) { + // FOR CLOSED : NEED TO BE STEWARD / OR MEMBER OF LIST (not ready on backend) + return this._securityService.isStewardOfOrgUnit(this.orgUnitResId).pipe(map(result => !result)); + } + } + + getTooltipDownload(isDisabled: boolean): undefined | string { + if (this.isPublicMetadata() || !isDisabled) { + return undefined; + } + if (this.isRestrictedMetadata()) { + return MARK_AS_TRANSLATABLE("homePage.archive.detail.buttonDisabledReason.needToBeMember"); + } + if (this.isClosedMetadata()) { + return MARK_AS_TRANSLATABLE("homePage.archive.detail.buttonDisabledReason.needToBeSteward"); + } } } diff --git a/src/app/features/home/stores/home.action.ts b/src/app/features/home/stores/home.action.ts index 16b828d88..3707f55f4 100644 --- a/src/app/features/home/stores/home.action.ts +++ b/src/app/features/home/stores/home.action.ts @@ -103,7 +103,7 @@ export namespace HomeAction { static readonly type: string = `[${state}] Download Success`; } - export class DownloadFail extends BaseSubAction<Download> { + export class DownloadFail extends BaseSubAction<DownloadStart> { static readonly type: string = `[${state}] Download Fail`; } diff --git a/src/app/features/home/stores/home.state.ts b/src/app/features/home/stores/home.state.ts index 6d3ea056f..3ef5271dc 100644 --- a/src/app/features/home/stores/home.state.ts +++ b/src/app/features/home/stores/home.state.ts @@ -391,8 +391,12 @@ export class HomeState extends BasicState<HomeStateModel> { const fileSize = archiveMetadata.metadata[MetadataEnum.aipSize]; this.downloadService.download(`${this.getUrlMetadata(ctx)}/${archiveMetadata.resId}/${ApiActionEnum.DL}`, fileName, fileSize) - .subscribe(() => { - ctx.dispatch(new HomeAction.DownloadSuccess(action)); + .subscribe((result) => { + if (result) { + ctx.dispatch(new HomeAction.DownloadSuccess(action)); + } else { + ctx.dispatch(new HomeAction.DownloadFail(action)); + } }); } diff --git a/src/app/shared/services/download.service.ts b/src/app/shared/services/download.service.ts index 41a244281..b05c8552d 100644 --- a/src/app/shared/services/download.service.ts +++ b/src/app/shared/services/download.service.ts @@ -11,6 +11,7 @@ import { } from "rxjs"; import {tap} from "rxjs/operators"; import { + HttpStatus, isNullOrUndefined, isTrue, OAuth2Service, @@ -25,7 +26,7 @@ export class DownloadService { constructor(private oauth2Service: OAuth2Service, private httpClient: HttpClient) {} - download(url: string, fileName: string, size?: number): Observable<void> { + download(url: string, fileName: string, size?: number): Observable<boolean> { if (isNullOrUndefined(streamSaver.WritableStream)) { streamSaver.WritableStream = WritableStream; } @@ -47,34 +48,40 @@ export class DownloadService { requestHeaders.set("Authorization", "Bearer " + token); } - return from( - fetch(url, { - headers: requestHeaders, - }).then(res => { - const readableStream = res.body; - // const readableStream = new ReadableStream(res.body); // TODO To allow optimize version on firefox but half work and break Chrome download - const writableStream = streamSaver.WritableStream; - // Optimized way for supported browser - if (writableStream && readableStream.pipeTo) { - return readableStream.pipeTo(fileStream) - .then(() => { - }); - } - - // Standard way - const writer = fileStream.getWriter(); - const reader = res.body.getReader(); - const pump = () => reader.read() - .then(result => { - if (result.done) { - return writer.close(); - } else { - return writer.write(result.value).then(pump); - } + const fetchResult = fetch(url, { + headers: requestHeaders, + }).then(res => { + if (res.status !== HttpStatus.OK) { + return false; + } + const readableStream = res.body; + // const readableStream = new ReadableStream(res.body); // TODO To allow optimize version on firefox but half work and break Chrome download + const writableStream = streamSaver.WritableStream; + // Optimized way for supported browser + if (writableStream && readableStream.pipeTo) { + return readableStream.pipeTo(fileStream) + .then(() => { + return true; }); - pump(); - }), - ); + } + + // Standard way + const writer = fileStream.getWriter(); + const reader = res.body.getReader(); + const pump = () => reader.read() + .then(result => { + if (result.done) { + writer.close(); + return true; + } else { + writer.write(result.value).then(pump); + return true; + } + }); + pump(); + }); + + return from(fetchResult); } // Prefer use download api to stream directly on disk diff --git a/src/app/shared/services/security.service.ts b/src/app/shared/services/security.service.ts index 0e6a94120..daa056402 100644 --- a/src/app/shared/services/security.service.ts +++ b/src/app/shared/services/security.service.ts @@ -28,6 +28,7 @@ import { }) export class SecurityService { + private readonly ORGUNIT_ROLE_NEED_ACCESS_TO_CLOSED_DATASET: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.STEWARD, Enums.Role.RoleEnum.MANAGER]; private readonly ORGUNIT_ROLE_NEED_TO_EDIT: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.MANAGER]; private readonly DEPOSIT_ROLE_NEED_TO_CREATE: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.MANAGER, Enums.Role.RoleEnum.STEWARD, Enums.Role.RoleEnum.APPROVER, Enums.Role.RoleEnum.CREATOR]; private readonly DEPOSIT_ROLE_NEED_TO_EDIT: Enums.Role.RoleEnum[] = [Enums.Role.RoleEnum.MANAGER, Enums.Role.RoleEnum.STEWARD, Enums.Role.RoleEnum.APPROVER, Enums.Role.RoleEnum.CREATOR]; @@ -59,6 +60,16 @@ export class SecurityService { return this.currentUserHaveRoleInListForOrgUnit(orgUnitId, this.ORGUNIT_ROLE_NEED_TO_EDIT); } + public isStewardOfOrgUnit(orgUnitId: string): Observable<boolean> { + if (this.isRootOrAdmin()) { + return of(true); + } + if (!this.isMemberOfOrgUnit(orgUnitId)) { + return of(false); + } + return this.currentUserHaveRoleInListForOrgUnit(orgUnitId, this.ORGUNIT_ROLE_NEED_ACCESS_TO_CLOSED_DATASET); + } + public canSeeDetailDeposit(deposit: Deposit): boolean { if (this.isRootOrAdmin()) { return true; diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 611257dbb..ee0922ea0 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1928,6 +1928,10 @@ "addToCart": "Zur Download-Bestellung hinzufügen", "askAccess": "Zugang anfordern" }, + "buttonDisabledReason": { + "needToBeMember": "Sie müssen Mitglied der Organisationseinheit sein, um diese Aktion durchführen zu können", + "needToBeSteward": "Sie müssen ein Steward der Organisationseinheit sein, um diese Aktion durchzuführen." + }, "datacite": "Datacite-Metadaten", "description": "Beschreibung", "dlcm": "Dlcm-Metadaten", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 00bd8cd13..61d62fa8e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1928,6 +1928,10 @@ "addToCart": "Add to download order", "askAccess": "Ask access" }, + "buttonDisabledReason": { + "needToBeMember": "You must be a member of the organizational unit to do this action", + "needToBeSteward": "You must be Steward of the organizational unit to do this action" + }, "datacite": "Datacite metadata", "description": "Description", "dlcm": "Dlcm metadata", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index d3f5fee07..face9e6fa 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -1928,6 +1928,10 @@ "addToCart": "Ajouter aux téléchargements", "askAccess": "Demander l'accès" }, + "buttonDisabledReason": { + "needToBeMember": "Vous devez être membre de l'unité organisationnelle pour faire cette action", + "needToBeSteward": "Vous devez être steward de l'unité organisationnelle pour faire cette action" + }, "datacite": "Datacite métadonnées", "description": "Description", "dlcm": "Dlcm métadonnées", -- GitLab