diff --git a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.html new file mode 100644 index 00000000000..e4966768477 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.html @@ -0,0 +1,28 @@ +
+ +
+ + +
+ +
+ + +
+ + {{ variant === 'full' && showDisclaimer ? ('item.page.cc.license.disclaimer' | translate) : '' }} + {{ name }} + +
+
+
+
diff --git a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.spec.ts new file mode 100644 index 00000000000..7a29fed2757 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.spec.ts @@ -0,0 +1,298 @@ +import { + ChangeDetectionStrategy, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; +import { Item } from 'src/app/core/shared/item.model'; +import { + MetadataMap, + MetadataValue, +} from 'src/app/core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; +import { createPaginatedList } from 'src/app/shared/testing/utils.test'; + +import { APP_CONFIG } from '../../../../../../config/app-config.interface'; +import { environment } from '../../../../../../environments/environment'; +import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; +import { ItemPageCcLicenseFieldComponent } from './item-page-cc-license-field.component'; + + +interface TestInstance { + metadata: { + 'dc.rights.uri'?: string; + 'dc.rights'?: string; + }; + componentInputs?: { + variant?: 'small' | 'full'; + showName?: boolean; + showDisclaimer?: boolean; + }; +} + + +interface TestCase { + testInstance: TestInstance; + expected: { + render: boolean, + showImage: boolean, + showName: boolean, + showDisclaimer: boolean + }; +} + + +const licenseNameMock = 'CC LICENSE NAME'; + + +const testCases: TestCase[] = [ + { + testInstance: { + metadata: { 'dc.rights.uri': undefined, 'dc.rights': undefined }, + }, + expected: { + render: false, + showName: false, + showImage: false, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': null, 'dc.rights': null }, + }, + expected: { + render: false, + showName: false, + showImage: false, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/licenses/by/4.0', 'dc.rights': null }, + }, + expected: { + render: false, + showName: false, + showImage: false, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': null, 'dc.rights': licenseNameMock }, + }, + expected: { + render: false, + showName: false, + showImage: false, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/licenses/by/4.0', 'dc.rights': licenseNameMock }, + }, + expected: { + render: true, + showName: true, + showImage: true, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/', 'dc.rights': licenseNameMock }, + }, + expected: { + render: true, + showName: true, + showImage: false, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/', 'dc.rights': licenseNameMock }, + componentInputs: { variant: 'full' }, + }, + expected: { + render: true, + showName: true, + showImage: false, + showDisclaimer: true, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/', 'dc.rights': licenseNameMock }, + componentInputs: { showName: false }, + }, + expected: { + render: true, + showName: true, + showImage: false, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/licenses/by/4.0', 'dc.rights': licenseNameMock }, + componentInputs: { showName: false }, + }, + expected: { + render: true, + showName: false, + showImage: true, + showDisclaimer: false, + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': 'https://creativecommons.org/licenses/by/4.0', 'dc.rights': licenseNameMock }, + componentInputs: { variant: 'full', showDisclaimer: false }, + }, + expected: { + render: true, + showName: true, + showImage: true, + showDisclaimer: false, + }, + }, +]; + + +// Updates the component fixture with parameters from the test instance +function configureFixture( + fixture: ComponentFixture, + testInstance: TestInstance, +) { + const item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: new MetadataMap(), + }); + + for (const [key, value] of Object.entries(testInstance.metadata)) { + item.metadata[key] = [ + { + language: 'en_US', + value: value, + }, + ] as MetadataValue[]; + } + + let component: ItemPageCcLicenseFieldComponent = fixture.componentInstance; + for (const [key, value] of Object.entries(testInstance.componentInputs ?? {})) { + component[key] = value; + } + component.item = item; + + fixture.detectChanges(); +} + + +describe('ItemPageCcLicenseFieldComponent', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + void TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + }, + }), + ItemPageCcLicenseFieldComponent, + ], + providers: [{ provide: APP_CONFIG, useValue: environment }], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(ItemPageCcLicenseFieldComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(ItemPageCcLicenseFieldComponent); + })); + + testCases.forEach((testCase) => { + describe('', () => { + beforeEach(async () => { + configureFixture(fixture, testCase.testInstance); + + // Waits the image to be loaded or to cause an error when loading + let imgEl = fixture.debugElement.query(By.css('img')); + if (imgEl) { + await new Promise((resolve, reject) => { + imgEl.nativeElement.addEventListener('load', () => resolve()); + imgEl.nativeElement.addEventListener('error', () => resolve()); + }); + } + + // Executes again because the 'img' element could have been updated due to a loading error + fixture.detectChanges(); + }); + + it('should render or not the component', + () => { + const componentEl = fixture.debugElement.query(By.css('.item-page-field')); + expect(Boolean(componentEl)).toBe(testCase.expected.render); + }, + ); + + it('should show/hide CC license name', + () => { + const nameEl = fixture.debugElement.query(de => de.nativeElement.id === 'cc-name'); + expect(Boolean(nameEl)).toBe(testCase.expected.showName); + if (nameEl && testCase.expected.showName) { + expect(nameEl.nativeElement.innerHTML).toContain(licenseNameMock); + } + }, + ); + + it('should show CC license image', + () => { + const imgEl = fixture.debugElement.query(By.css('img')); + expect(Boolean(imgEl)).toBe(testCase.expected.showImage); + }, + ); + + it('should use name fallback when CC image fails loading', + () => { + const nameEl = fixture.debugElement.query(de => de.nativeElement.id === 'cc-name'); + expect(Boolean(nameEl)).toBe(testCase.expected.showName); + if (nameEl && testCase.expected.showName) { + expect(nameEl.nativeElement.innerHTML).toContain(licenseNameMock); + } + }, + ); + + it('should show or not CC license disclaimer', + () => { + const disclaimerEl = fixture.debugElement.query(By.css('span')); + if (testCase.expected.showDisclaimer) { + expect(disclaimerEl).toBeTruthy(); + expect(disclaimerEl.nativeElement.innerHTML).toContain('item.page.cc.license.disclaimer'); + } else if (testCase.expected.render) { + expect(disclaimerEl).toBeTruthy(); + expect(disclaimerEl.nativeElement.innerHTML).not.toContain('item.page.cc.license.disclaimer'); + } else { + expect(disclaimerEl).toBeFalsy(); + } + }, + ); + }); + }); +}); diff --git a/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts new file mode 100644 index 00000000000..5d19fe4b4e2 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts @@ -0,0 +1,73 @@ +import { + NgClass, + NgIf, + NgStyle, +} from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { Item } from 'src/app/core/shared/item.model'; +import { MetadataFieldWrapperComponent } from 'src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; + +@Component({ + selector: 'ds-item-page-cc-license-field', + templateUrl: './item-page-cc-license-field.component.html', + standalone: true, + imports: [NgIf, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent], +}) +/** + * Displays the item's Creative Commons license image in it's simple item page + */ +export class ItemPageCcLicenseFieldComponent implements OnInit { + /** + * The item to display the CC license image for + */ + @Input() item: Item; + + /** + * 'full' variant shows image, a disclaimer (optional) and name (always), better for the item page content. + * 'small' variant shows image and name (optional), better for the item page sidebar + */ + @Input() variant?: 'small' | 'full' = 'small'; + + /** + * Filed name containing the CC license URI, as configured in the back-end, in the 'dspace.cfg' file, propertie + * 'cc.license.uri' + */ + @Input() ccLicenseUriField? = 'dc.rights.uri'; + + /** + * Filed name containing the CC license name, as configured in the back-end, in the 'dspace.cfg' file, propertie + * 'cc.license.name' + */ + @Input() ccLicenseNameField? = 'dc.rights'; + + /** + * Shows the CC license name with the image. Always show if image fails to load + */ + @Input() showName? = true; + + /** + * Shows the disclaimer in the 'full' variant of the component + */ + @Input() showDisclaimer? = true; + + uri: string; + name: string; + showImage = true; + imgSrc: string; + + ngOnInit() { + this.uri = this.item.firstMetadataValue(this.ccLicenseUriField); + this.name = this.item.firstMetadataValue(this.ccLicenseNameField); + + // Extracts the CC license code from the URI + const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; + const matches = regex.exec(this.uri ?? '') ?? []; + const ccCode = matches.length > 2 ? matches[2] : null; + this.imgSrc = ccCode ? `assets/images/cc-licenses/${ccCode}.png` : null; + } +} diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index cd1c0c445ce..f7e44ab14c4 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -86,6 +86,8 @@ [fields]="['datacite.relation.isReferencedBy']" [label]="'item.page.dataset'"> + +
{{"item.page.link.full" | translate}} diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts index 74b4b50875b..a9ef9b2eadf 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts @@ -21,6 +21,7 @@ import { ThemedMediaViewerComponent } from '../../../media-viewer/themed-media-v import { MiradorViewerComponent } from '../../../mirador-viewer/mirador-viewer.component'; import { ThemedFileSectionComponent } from '../../field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../field-components/specific-field/abstract/item-page-abstract-field.component'; +import { ItemPageCcLicenseFieldComponent } from '../../field-components/specific-field/cc-license/item-page-cc-license-field.component'; import { ItemPageDateFieldComponent } from '../../field-components/specific-field/date/item-page-date-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../field-components/specific-field/title/themed-item-page-field.component'; @@ -39,8 +40,26 @@ import { ItemComponent } from '../shared/item.component'; templateUrl: './untyped-item.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, ThemedResultsBackButtonComponent, MiradorViewerComponent, ThemedItemPageTitleFieldComponent, DsoEditMenuComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, ThemedMediaViewerComponent, ThemedFileSectionComponent, ItemPageDateFieldComponent, ThemedMetadataRepresentationListComponent, GenericItemPageFieldComponent, ItemPageAbstractFieldComponent, ItemPageUriFieldComponent, CollectionsComponent, RouterLink, AsyncPipe, TranslateModule], + imports: [ + NgIf, + ThemedResultsBackButtonComponent, + MiradorViewerComponent, + ThemedItemPageTitleFieldComponent, + DsoEditMenuComponent, + MetadataFieldWrapperComponent, + ThemedThumbnailComponent, + ThemedMediaViewerComponent, + ThemedFileSectionComponent, + ItemPageDateFieldComponent, + ThemedMetadataRepresentationListComponent, + GenericItemPageFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + CollectionsComponent, + RouterLink, + AsyncPipe, + TranslateModule, + ItemPageCcLicenseFieldComponent, + ], }) -export class UntypedItemComponent extends ItemComponent { - -} +export class UntypedItemComponent extends ItemComponent {} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index fa956491c69..6a4857f4e00 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6715,4 +6715,8 @@ "search.filters.filter.notifyEndorsement.placeholder": "Notify Endorsement", "search.filters.filter.notifyEndorsement.label": "Search Notify Endorsement", + + "item.page.cc.license.title": "Creative Commons license", + + "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as", } diff --git a/src/assets/images/cc-licenses/by-nc-nd.png b/src/assets/images/cc-licenses/by-nc-nd.png new file mode 100644 index 00000000000..5a8dbd0652e Binary files /dev/null and b/src/assets/images/cc-licenses/by-nc-nd.png differ diff --git a/src/assets/images/cc-licenses/by-nc-sa.png b/src/assets/images/cc-licenses/by-nc-sa.png new file mode 100644 index 00000000000..b9a55533c0c Binary files /dev/null and b/src/assets/images/cc-licenses/by-nc-sa.png differ diff --git a/src/assets/images/cc-licenses/by-nc.png b/src/assets/images/cc-licenses/by-nc.png new file mode 100644 index 00000000000..25e284099a0 Binary files /dev/null and b/src/assets/images/cc-licenses/by-nc.png differ diff --git a/src/assets/images/cc-licenses/by-nd.png b/src/assets/images/cc-licenses/by-nd.png new file mode 100644 index 00000000000..fc3d26789a0 Binary files /dev/null and b/src/assets/images/cc-licenses/by-nd.png differ diff --git a/src/assets/images/cc-licenses/by-sa.png b/src/assets/images/cc-licenses/by-sa.png new file mode 100644 index 00000000000..8770732928c Binary files /dev/null and b/src/assets/images/cc-licenses/by-sa.png differ diff --git a/src/assets/images/cc-licenses/by.png b/src/assets/images/cc-licenses/by.png new file mode 100644 index 00000000000..c8473a24786 Binary files /dev/null and b/src/assets/images/cc-licenses/by.png differ diff --git a/src/assets/images/cc-licenses/zero.png b/src/assets/images/cc-licenses/zero.png new file mode 100644 index 00000000000..4ff09a0bb26 Binary files /dev/null and b/src/assets/images/cc-licenses/zero.png differ diff --git a/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts b/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts index aeae9a519a5..6fbb035d2f2 100644 --- a/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts +++ b/src/themes/custom/app/item-page/simple/item-types/untyped-item/untyped-item.component.ts @@ -17,6 +17,7 @@ import { ThemedMediaViewerComponent } from '../../../../../../../app/item-page/m import { MiradorViewerComponent } from '../../../../../../../app/item-page/mirador-viewer/mirador-viewer.component'; import { ThemedFileSectionComponent } from '../../../../../../../app/item-page/simple/field-components/file-section/themed-file-section.component'; import { ItemPageAbstractFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component'; +import { ItemPageCcLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/date/item-page-date-field.component'; import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; @@ -36,12 +37,34 @@ import { ThemedThumbnailComponent } from '../../../../../../../app/thumbnail/the @Component({ selector: 'ds-untyped-item', // styleUrls: ['./untyped-item.component.scss'], - styleUrls: ['../../../../../../../app/item-page/simple/item-types/untyped-item/untyped-item.component.scss'], + styleUrls: [ + '../../../../../../../app/item-page/simple/item-types/untyped-item/untyped-item.component.scss', + ], // templateUrl: './untyped-item.component.html', - templateUrl: '../../../../../../../app/item-page/simple/item-types/untyped-item/untyped-item.component.html', + templateUrl: + '../../../../../../../app/item-page/simple/item-types/untyped-item/untyped-item.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, ThemedResultsBackButtonComponent, MiradorViewerComponent, ThemedItemPageTitleFieldComponent, DsoEditMenuComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, ThemedMediaViewerComponent, ThemedFileSectionComponent, ItemPageDateFieldComponent, ThemedMetadataRepresentationListComponent, GenericItemPageFieldComponent, ItemPageAbstractFieldComponent, ItemPageUriFieldComponent, CollectionsComponent, RouterLink, AsyncPipe, TranslateModule], + imports: [ + NgIf, + ThemedResultsBackButtonComponent, + MiradorViewerComponent, + ThemedItemPageTitleFieldComponent, + DsoEditMenuComponent, + MetadataFieldWrapperComponent, + ThemedThumbnailComponent, + ThemedMediaViewerComponent, + ThemedFileSectionComponent, + ItemPageDateFieldComponent, + ThemedMetadataRepresentationListComponent, + GenericItemPageFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + CollectionsComponent, + RouterLink, + AsyncPipe, + TranslateModule, + ItemPageCcLicenseFieldComponent, + ], }) -export class UntypedItemComponent extends BaseComponent { -} +export class UntypedItemComponent extends BaseComponent {}