From c9ad7e3d70ed63ca1b6aa0bb3c1aaa2b72ca43ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20G=C3=B3mez?= Date: Fri, 20 Sep 2024 15:07:27 +0200 Subject: [PATCH] Add ItemPageCcLicenseFieldComponent to render rights information --- .../item-page-license-field.component.html | 29 ++ .../item-page-license-field.component.spec.ts | 280 ++++++++++++++++++ .../item-page-license-field.component.ts | 85 ++++++ .../license/item-page-license-field.scss | 4 + src/assets/i18n/en.json5 | 2 + 5 files changed, 400 insertions(+) create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts create mode 100644 src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html new file mode 100644 index 00000000000..2f07cda100b --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.html @@ -0,0 +1,29 @@ +
+ + + + + + + + + + + + {{ license }} + {{ separator }} + + + + + {{ license }} + {{ separator }} + + + {{ uri }} + {{ separator }} + + + + +
diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts new file mode 100644 index 00000000000..2dbb6f09822 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.spec.ts @@ -0,0 +1,280 @@ +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 { ItemPageLicenseFieldComponent } from './item-page-license-field.component'; + + +interface TestInstance { + metadata: { + }; +} + + +interface TestCase { + testInstance: TestInstance; + expected: { + render: boolean, + textElements: string[], + linkElements: string[], + }; +} + + +const licenseNameMock = 'LICENSE NAME'; +const exampleUriMock = 'http://example.com'; +const ccUriMock = 'https://creativecommons.org/licenses/by/4.0'; + + +const testCases: TestCase[] = [ + { + testInstance: { + metadata: { 'dc.rights': undefined, 'dc.rights.uri': undefined, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': [ undefined, undefined ], 'dc.rights.uri': undefined, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights.license': undefined, 'dc.rights.uri': undefined, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': undefined, 'dc.rights.license': undefined, 'dc.rights.uri': [ undefined, undefined ], }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': null, }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': [ null, null ], }, + }, + expected: { + render: false, + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': exampleUriMock, }, + }, + expected: { + render: true, + textElements: [exampleUriMock], + linkElements: [exampleUriMock], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': null, 'dc.rights.uri': exampleUriMock, }, + }, + expected: { + render: true, + textElements: [exampleUriMock], + linkElements: [exampleUriMock], + }, + }, + { + testInstance: { + metadata: { 'dc.rights.uri': ccUriMock, }, + }, + expected: { + render: true, + textElements: [ccUriMock], + linkElements: [ccUriMock], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': null, 'dc.rights.license': licenseNameMock, 'dc.rights.uri': null }, + }, + expected: { + render: true, + textElements: [licenseNameMock], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.uri': ccUriMock, }, + }, + expected: { + render: true, + // This test case is delegated to ItemPageCcLicenseFieldComponent + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.license': licenseNameMock, 'dc.rights.uri': ccUriMock }, + }, + expected: { + render: true, + // This test case meets the CC criteria too (since it has 'dc.rights', and 'dc.rights.uri' + // points to a CC license). Thus, it is delegated to ItemPageCcLicenseFieldComponent. + textElements: [], + linkElements: [], + }, + }, + { + testInstance: { + metadata: { 'dc.rights': licenseNameMock, 'dc.rights.license': licenseNameMock, 'dc.rights.uri': exampleUriMock }, + }, + expected: { + render: true, + textElements: [licenseNameMock, licenseNameMock, exampleUriMock], + linkElements: [exampleUriMock], + }, + }, +]; + + +// 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, values] of Object.entries(testInstance.metadata)) { + for (const value of values instanceof Array ? values : [values]) { + item.metadata[key] = [ + { + language: 'en_US', + value: value, + }, + ] as MetadataValue[]; + } + } + + let component: ItemPageLicenseFieldComponent = fixture.componentInstance; + component.item = item; + + fixture.detectChanges(); +} + + +describe('ItemPageLicenseFieldComponent', () => { + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + void TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + }, + }), + ItemPageLicenseFieldComponent, + ], + providers: [{ provide: APP_CONFIG, useValue: environment }], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(ItemPageLicenseFieldComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(ItemPageLicenseFieldComponent); + })); + + testCases.forEach((testCase) => { + describe('', () => { + beforeEach(async () => { + configureFixture(fixture, testCase.testInstance); + }); + + 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 license as plain text', + () => { + const textEl = fixture.debugElement.queryAll(By.css('.license-text')); + expect(textEl.length).toBe(testCase.expected.textElements.length); + if (textEl && testCase.expected.textElements.length > 0) { + textEl.forEach((elt, idx) => + expect(elt.nativeElement.innerHTML).toContain(testCase.expected.textElements[idx]) + ); + } + }, + ); + + it('should show/hide the license as link', + () => { + const linkEl = fixture.debugElement.queryAll(By.css('.license-link')); + expect(linkEl.length).toBe(testCase.expected.linkElements.length); + if (linkEl && testCase.expected.linkElements.length > 0) { + linkEl.forEach((elt, idx) => + expect(elt.query(By.css('.license-text')).nativeElement.innerHTML).toContain(testCase.expected.linkElements[idx]) + ); + } + }, + ); + }); + }); +}); diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts new file mode 100644 index 00000000000..4a559ffaa85 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.component.ts @@ -0,0 +1,85 @@ +import { + NgClass, + NgIf, + NgFor, + NgStyle, +} from '@angular/common'; +import { + Component, + Input, + OnInit, + ViewContainerRef, +} 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'; +import { ItemPageCcLicenseFieldComponent } from 'src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component'; +import { Metadata } from 'src/app/core/shared/metadata.utils'; + +@Component({ + selector: 'ds-item-page-license-field', + templateUrl: './item-page-license-field.component.html', + styleUrl: './item-page-license-field.scss', + standalone: true, + imports: [NgIf, NgFor, NgClass, NgStyle, TranslateModule, MetadataFieldWrapperComponent, ItemPageCcLicenseFieldComponent], +}) +/** + * Displays the item's licenses + * + * If the number of 'dc.rights*' values (excepting 'dc.rights.uri') and the number of 'dc.rights.uri' + * match, they will be printed as a list of links, where the text of the link will be the dc.right* + * value and the link the corresponding 'dc.rights.uri'. The match will be done in the order they + * appear. In any other case, all the 'dc.rights*' fields will be shown as a list (where the URIs + * will be rendered as links). + */ +export class ItemPageLicenseFieldComponent implements OnInit { + /** + * The item to display the license for + */ + @Input() item: Item; + + /** + * String to use as a separator if multiple license entries are specified + */ + @Input() separator: String = '•'; + + uris: string[]; + licenses: string[]; + isCcLicense: boolean; + + constructor(private viewRef: ViewContainerRef) {} + + ngOnInit() { + // This is a workaround to determine which fields are relevant to render a CC license + // until https://github.com/DSpace/dspace-angular/pull/3165 is merged in the main branch + // (which will provide a configuration property). + // Until then, we must dynamically create a ItemPageCcLicenseFieldComponent to retrieve + // the fields that are relevant to render a CC license field (since developers may have + // customized them directly in the code). + // This approach avoids hardcoding the values (thus breaking developers' customizations) + // and makes this code compatible with a future merge of the above PR + const ccComponentRef = this.viewRef.createComponent(ItemPageCcLicenseFieldComponent); + ccComponentRef.setInput('item', this.item); + // The regex below has been copied from ItemPageCcLicenseFieldComponent + // We duplicate the regex here to avoid changing the implementation of + // ItemPageCcLicenseFieldComponent to avoid breaking changes. + // It may be desirable to further refactor this code in the future + const regex = /.*creativecommons.org\/(licenses|publicdomain)\/([^/]+)/gm; + + // If the license information is a CC License, we will delegate the rendering + // of this field to ItemPageCcLicenseFieldComponent + this.isCcLicense = this.item.allMetadataValues(ccComponentRef.instance.ccLicenseUriField).length == 1 + && regex.exec(this.item.firstMetadataValue(ccComponentRef.instance.ccLicenseUriField)) != null + && this.item.allMetadataValues(ccComponentRef.instance.ccLicenseNameField).length == 1; + // We no longer need the ItemPageCcLicenseFieldComponent (and we don't want it to be rendered here) + ccComponentRef.destroy(); + + // In either case... + // get all non-empty dc.rights* values, excepting the URIs... + this.licenses = Metadata.all(this.item.metadata, Object.keys(this.item.metadata).filter(key => + key != 'dc.rights.uri' && (key.startsWith('dc.rights') || key.startsWith('dc.rights.'))) + ).map(mdValue => mdValue.value).filter(value => value); + // and get the URIs + this.uris = this.item.allMetadataValues('dc.rights.uri').filter(value => value); + } +} diff --git a/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss new file mode 100644 index 00000000000..c81c3055053 --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/license/item-page-license-field.scss @@ -0,0 +1,4 @@ +.separator { + padding-left: 0.5rem; + padding-right: 0.5rem; +} \ No newline at end of file diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 8f20c657f94..da3dd093cfe 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6734,6 +6734,8 @@ "search.filters.filter.notifyEndorsement.label": "Search Notify Endorsement", + "item.page.license.title": "Rights and licensing", + "item.page.cc.license.title": "Creative Commons license", "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as",