diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 334e5531fd7..a027cccabd7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -219,7 +219,7 @@ jobs: playwright-after-import8: - runs-on: ubuntu-latest + runs-on: dspace-test-1 needs: import-8 if: inputs.IMPORT timeout-minutes: 45 diff --git a/.github/workflows/trigger-ui-tests.yml b/.github/workflows/trigger-ui-tests.yml index 22dfc03fdf6..eb1764b706c 100644 --- a/.github/workflows/trigger-ui-tests.yml +++ b/.github/workflows/trigger-ui-tests.yml @@ -24,7 +24,14 @@ jobs: git fetch --prune origin BRANCHES=$(git ls-remote --heads origin | awk -F'/' '{print $3"/"$4}' | grep '^customer/') + SKIP_BRANCHES=("customer/sav" "customer/vsb-tuo" "customer/zcu-data") + for branch in $(echo "$BRANCHES" | sed -e 's/[\[\]"]//g' -e 's/,/\n/g'); do + if [[ " ${SKIP_BRANCHES[@]} " =~ " ${branch} " ]]; then + echo "Skipping branch $branch" + continue + fi + echo "Triggering UI tests for branch: $branch" gh workflow run playwright-tests.yml --ref $branch - done \ No newline at end of file + done diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index f281be8a7cc..06ccad3caac 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -10,6 +10,7 @@ # This is based heavily on the docker-compose.yml that is available in the DSpace/DSpace # (Backend) at: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml +version: '3.7' networks: dspacenet: # Due to the following specification, THIS FILE (docker-compose-rest.yml) must be last (if using several YMLs), diff --git a/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts b/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts index c205ef73242..fb66d19ea98 100644 --- a/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts +++ b/src/app/bitstream-page/clarin-license-agreement-page/clarin-license-agreement-page.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { finalize, switchMap, take } from 'rxjs/operators'; +import { BehaviorSubject, firstValueFrom, Observable, of as observableOf } from 'rxjs'; +import { filter, finalize, switchMap, take } from 'rxjs/operators'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { ClarinUserRegistration } from '../../core/shared/clarin/clarin-user-registration.model'; import { ClarinUserMetadata } from '../../core/shared/clarin/clarin-user-metadata.model'; @@ -35,13 +35,13 @@ import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-bu import { HttpOptions } from '../../core/dspace-rest/dspace-rest.service'; import { Router } from '@angular/router'; import { getItemPageRoute } from '../../item-page/item-page-routing-paths'; -import { getBitstreamContentRoute } from '../../app-routing-paths'; import { hasFailed } from 'src/app/core/data/request-entry-state.model'; import { FindListOptions } from '../../core/data/find-list-options.model'; import isEqual from 'lodash/isEqual'; import cloneDeep from 'lodash/cloneDeep'; import { ClarinUserMetadataDataService } from '../../core/data/clarin/clarin-user-metadata.service'; import { HtmlContentService } from '../../shared/html-content.service'; +import { FileService } from '../../core/shared/file.service'; /** * The component shows the user's filled in user metadata and the user can fill in other required user metadata. @@ -143,7 +143,9 @@ export class ClarinLicenseAgreementPageComponent implements OnInit { private hardRedirectService: HardRedirectService, private requestService: RequestService, private clarinUserMetadataDataService: ClarinUserMetadataDataService, - private htmlContentService: HtmlContentService) { } + private htmlContentService: HtmlContentService, + protected fileService: FileService, + protected notificationsService: NotificationsService) { } ngOnInit(): void { // Load CurrentItem by bitstreamID to show itemHandle @@ -242,7 +244,7 @@ export class ClarinLicenseAgreementPageComponent implements OnInit { } else { // Or just download the bitstream by download token const downloadToken = Object.values(responseRD$?.payload).join(''); - this.redirectToDownload(downloadToken); + void this.redirectToDownload(downloadToken); } }); } @@ -255,23 +257,33 @@ export class ClarinLicenseAgreementPageComponent implements OnInit { return this.router.routerState.snapshot.url.endsWith('/zip'); } - private redirectToDownload(downloadToken = null) { - // 1. Get bitstream - // 2. Get bitstream download link - // 3. Get bitstream content download link and check if there is `authorization-token` in to query params - let bitstream = null; - this.bitstream$ - .pipe(take(1)) - .subscribe(bitstream$ => { - bitstream = bitstream$; - }); - let bitstreamDownloadPath = getBitstreamContentRoute(bitstream); - if (isNotEmpty(downloadToken)) { - bitstreamDownloadPath = this.halService.getRootHref() + '/core' + bitstreamDownloadPath + - '?dtoken=' + downloadToken; + /** + * Redirects to the download link of the bitstream. + * If a download token is provided, it appends it as a query parameter. + * + * @param downloadToken + * @private + */ + private async redirectToDownload(downloadToken?: string): Promise { + try { + const bitstream = await firstValueFrom(this.bitstream$.pipe(take(1))); + + const fileLink = await firstValueFrom( + this.fileService.retrieveFileDownloadLink(bitstream._links.content.href).pipe( + filter(hasValue), + take(1) + ) + ); + + // Determine whether the URL already contains query parameters + const hasQueryParams = fileLink.includes('?'); + const tokenParam = downloadToken ? `${hasQueryParams ? '&' : '?'}dtoken=${downloadToken}` : ''; + + const redirectUrl = `${fileLink}${tokenParam}`; + this.hardRedirectService.redirect(redirectUrl); + } catch (error) { + this.notificationsService.error(this.translateService.instant('clarin-license-agreement-page.download-error')); } - - this.hardRedirectService.redirect(bitstreamDownloadPath); } public getMetadataValueByKey(metadataKey: string) { diff --git a/src/app/core/data/dso-redirect.service.ts b/src/app/core/data/dso-redirect.service.ts index 431f7fb4be4..2010b753d88 100644 --- a/src/app/core/data/dso-redirect.service.ts +++ b/src/app/core/data/dso-redirect.service.ts @@ -8,7 +8,7 @@ /* eslint-disable max-classes-per-file */ import { Injectable, Inject } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { take, tap } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -19,7 +19,7 @@ import { RequestService } from './request.service'; import { getFirstCompletedRemoteData } from '../shared/operators'; import { DSpaceObject } from '../shared/dspace-object.model'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { getDSORoute } from '../../app-routing-paths'; +import { getDSORoute, getForbiddenRoute } from '../../app-routing-paths'; import { HardRedirectService } from '../services/hard-redirect.service'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { Router } from '@angular/router'; @@ -45,7 +45,7 @@ class DsoByIdOrUUIDDataService extends IdentifiableDataService { // interpolate id/uuid as query parameter (endpoint: string, resourceID: string): string => { return endpoint.replace(/{\?id}/, `?id=${resourceID}`) - .replace(/{\?uuid}/, `?uuid=${resourceID}`); + .replace(/{\?uuid}/, `?uuid=${resourceID}`); }, ); } @@ -108,12 +108,23 @@ export class DsoRedirectService { } } } - // Redirect to login page if the user is not authenticated to see the requested page + // Handle authentication errors: redirect unauthenticated users to login, authenticated users to forbidden page if (response.hasFailed && (response.statusCode === 401 || response.statusCode === 403)) { - // Extract redirect URL - remove `https://.../namespace` from the current URL. Keep only `handle/...` - const redirectUrl = this.extractHandlePath(window.location.href); - this.authService.setRedirectUrl(redirectUrl); - this.router.navigateByUrl('login'); + const isAuthenticated$ = this.authService.isAuthenticated(); + isAuthenticated$ + .pipe(take(1)) + .subscribe((isAuthenticated) => { + if (!isAuthenticated) { + // If the user is not authenticated, redirect to login page + // Extract redirect URL - remove `https://.../namespace` from the current URL. Keep only `handle/...` + const redirectUrl = this.extractHandlePath(window.location.href); + this.authService.setRedirectUrl(redirectUrl); + void this.router.navigateByUrl('login'); + } else { + // If the user is authenticated but still has no access, redirect to forbidden page + void this.router.navigateByUrl(getForbiddenRoute()); + } + }); } }) ); diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html index 407f1963e93..ac481810c59 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.html @@ -11,11 +11,7 @@
-
- {{citationText + ', '}} - {{itemNameText + ', '}} - {{repositoryNameText + ', '}} - {{prettifiedIdentifier | async}} +
diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index a9cdbb1f0e5..94980315738 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -1,7 +1,6 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Item } from '../../core/shared/item.model'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { isEmpty, isNotEmpty, isNull, isUndefined } from '../../shared/empty.util'; import { getFirstSucceededRemoteData } from '../../core/shared/operators'; import { Clipboard } from '@angular/cdk/clipboard'; import { NgbModal, NgbTooltip, NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; @@ -11,16 +10,8 @@ import { RequestService } from '../../core/data/request.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { BehaviorSubject } from 'rxjs'; -import { - DOI_METADATA_FIELD, HANDLE_METADATA_FIELD, -} from '../simple/field-components/clarin-generic-item-field/clarin-generic-item-field.component'; -import { ItemIdentifierService } from '../../shared/item-identifier.service'; -import { AUTHOR_METADATA_FIELDS } from '../../core/shared/clarin/constants'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -/** - * If the item has more authors do not add all authors to the citation but add there a shortcut. - */ -export const ET_AL_TEXT = 'et al.'; /** * The citation part in the ref-box component. @@ -44,38 +35,26 @@ export class ClarinRefCitationComponent implements OnInit { */ @ViewChild('tooltip', {static: false}) tooltipRef: NgbTooltip; - /** - * The parameters retrieved from the Item metadata for creating the citation in the proper way. - */ - /** - * Author and issued year - */ - citationText: string; - /** - * Whole Handle URI - */ - identifierURI: string; /** * Name of the Item */ itemNameText: string; + /** - * The nam of the organization which provides the repository - */ - repositoryNameText: string; - /** - * BehaviorSubject to store the prettified identifier. + * The content of the reference box, which will be displayed in the tooltip. + * This content is fetched from the RefBox Controller. */ - prettifiedIdentifier: BehaviorSubject = new BehaviorSubject(null); + refboxContent: BehaviorSubject = new BehaviorSubject(null); + /** - * The item has DOI or not. + * The raw content of the reference box, which is fetched from the RefBox Controller. */ - hasDoi = false; + refboxCopyContent = ''; /** - * The authors of the item. Fetched from the metadata. + * The text to be displayed when the ref box content is empty or cannot be fetched. */ - authors: string[] = []; + EMPTY_CONTENT = 'Cannot fetch the ref box content'; constructor(private configurationService: ConfigurationDataService, private clipboard: Clipboard, @@ -84,146 +63,59 @@ export class ClarinRefCitationComponent implements OnInit { private requestService: RequestService, protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService, - private itemIdentifierService: ItemIdentifierService) { + private sanitizer: DomSanitizer) { // Configure the tooltip to show on click - `Copied` message config.triggers = 'click'; } ngOnInit(): void { - this.authors = this.item.allMetadataValues(AUTHOR_METADATA_FIELDS); - // First Part could be authors or publisher - let firstPart = this.getAuthors(); - const year = this.getYear(); - - // Show publisher instead of author if author is none - if (isEmpty(firstPart)) { - firstPart = this.item.firstMetadataValue('dc.publisher'); - } - - let citationArray = [firstPart, year]; - // Filter null values - citationArray = citationArray.filter(textValue => { - return isNotEmpty(textValue); - }); - - this.hasDoi = this.hasItemDoi(); - this.citationText = citationArray.join(', '); - this.itemNameText = this.getTitle(); - this.identifierURI = this.getIdentifierUri(this.whichIdentifierMetadataField()); - void this.itemIdentifierService.prettifyIdentifier(this.identifierURI, [this.whichIdentifierMetadataField()]) - .then((value: string) => { - this.prettifiedIdentifier.next(value); + void this.fetchRefBoxContent() + .then((content) => { + this.refboxCopyContent = content; // Store raw HTML + this.refboxContent.next(this.sanitizer.bypassSecurityTrustHtml(content)); + }).catch((error) => { + console.error('Failed to fetch refbox content:', error); + this.refboxCopyContent = this.EMPTY_CONTENT; + this.refboxContent.next(this.EMPTY_CONTENT); }); - void this.getRepositoryName().then(res => { - this.repositoryNameText = res?.payload?.values?.[0]; - }); + this.itemNameText = this.item?.firstMetadataValue('dc.title'); } /** - * After click on the `Copy` icon the text will be formatted and copied for the user. + * Copy the text from the reference box to the clipboard. + * Remove the html tags from the text and copy only the plain text. */ copyText() { - const tabChar = ' '; - let authorWithItemName = this.citationText + ',\n' + tabChar + this.itemNameText; - this.clipboard.copy(authorWithItemName + ', ' + - this.repositoryNameText + ', \n' + tabChar + this.identifierURI); + let plainText = this.EMPTY_CONTENT; + if (this.refboxCopyContent) { + const parser = new DOMParser(); + const doc = parser.parseFromString(this.refboxCopyContent, 'text/html'); + plainText = doc.body.textContent || ''; + } + this.clipboard.copy(plainText); setTimeout(() => { this.tooltipRef.close(); }, 700); } - getRepositoryName(): Promise { - return this.configurationService.findByPropertyName('dspace.name') - .pipe(getFirstSucceededRemoteData()).toPromise(); - } - - /** - * Get the identifier URI from the item metadata. If the item has DOI, return the DOI, otherwise return the handle. - */ - getIdentifierUri(identifierMetadataField) { - return this.item.firstMetadataValue(identifierMetadataField); - } - - /** - * Check if the item has DOI. - */ - hasItemDoi() { - return this.item?.allMetadata(DOI_METADATA_FIELD)?.length > 0; - } - - /** - * If the item has DOI, return the DOI metadata field, otherwise return the handle metadata field. - */ - whichIdentifierMetadataField() { - return this.hasDoi ? DOI_METADATA_FIELD : HANDLE_METADATA_FIELD; - } - - getHandle() { - // Separate the handle from the full URI - const fullUri = this.getIdentifierUri(this.whichIdentifierMetadataField()); - const handleWord = 'handle/'; - const startHandleIndex = fullUri.indexOf('handle/') + handleWord.length; - return fullUri.substr(startHandleIndex); - } - /** - * Check if the Item has any author metadata. - * @param authorMetadata + * Fetch the content of the reference box from the RefBox Controller. */ - hasNoAuthor(authorMetadata: string[] = []) { - return isEmpty(authorMetadata); - } - - getAuthors() { - let authorText = ''; - const authorMetadata = this.authors; - if (isUndefined(authorMetadata) || isNull(authorMetadata)) { - return null; - } - - // If metadata value is `(:unav) Unknown author` return null - if (this.hasNoAuthor(authorMetadata)) { - return null; - } - - // If there is only one author - if (authorMetadata.length === 1) { - return authorMetadata[0]; - } - - // If there are less than 5 authors - if (authorMetadata.length <= 5) { - let authors_list = authorMetadata.join('; '); - // Replace last `;` with `and` - authors_list = authors_list.replace(/;([^;]*)$/, ' and$1'); - return authors_list; - } - - // If there are more than 5 authors - // Get only first author and add `et al.` at the end - authorText = authorMetadata[0] + '; ' + ET_AL_TEXT; - return authorText; - } - - getYear() { - const yearMetadata = this.item.metadata['dc.date.issued']; - if (isUndefined(yearMetadata) || isNull(yearMetadata)) { - return null; - } - - // The issued date is in the format '2000-01-01' - const issuedDateValues = yearMetadata[0]?.value?.split('-'); - // Extract the year and return - return issuedDateValues[0]; - } + async fetchRefBoxContent(): Promise { + const requestId = this.requestService.generateRequestId(); + const getRequest = new GetRequest( + requestId, + this.halService.getRootHref() + '/core/refbox?handle=' + this.item?.handle + ); + this.requestService.send(getRequest); - getTitle() { - const titleMetadata = this.item.metadata['dc.title']; - if (isUndefined(titleMetadata) || isNull(titleMetadata)) { - return null; + try { + const res: any = await this.rdbService.buildFromRequestUUID(requestId) + .pipe(getFirstSucceededRemoteData()).toPromise(); + return res?.payload?.displayText || this.EMPTY_CONTENT; + } catch (error) { + return this.EMPTY_CONTENT; } - - return titleMetadata[0]?.value; } /** @@ -259,8 +151,7 @@ export class ClarinRefCitationComponent implements OnInit { const requestId = this.requestService.generateRequestId(); // Create the request const getRequest = new GetRequest(requestId, this.halService.getRootHref() + '/core/refbox/citations?type=' + - // citationType + '&handle=' + this.getHandle(), requestOptions); - citationType + '&handle=' + this.getHandle()); + citationType + '&handle=' + this.item?.handle); // Call get request this.requestService.send(getRequest); diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html index d0052b9355b..bd34d965e28 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html @@ -1,6 +1,6 @@
- +

License Agreement

for Czech National Corpus Data


@@ -10,17 +14,15 @@

for Czech National Corpus Data

The Proprietor:
- Institute of the Czech National Corpus
Faculty of Arts
Charles University
Nám. Jana Palacha 2
CZ–116 38 Praha 1
- Czech Republic
- http://ucnk.ff.cuni.cz/ + Czech Republic

- and + and
@@ -73,4 +75,4 @@

for Czech National Corpus Data

  • This agreement is governed by the laws of the Czech Republic and all disputes concerning this agreement will be resolved by its jurisdiction.

  • -
    \ No newline at end of file +