Skip to content

Commit

Permalink
Add ItemPageCcLicenseFieldComponent to render rights information
Browse files Browse the repository at this point in the history
  • Loading branch information
abelgomez committed Sep 20, 2024
1 parent 9dec50e commit c9ad7e3
Show file tree
Hide file tree
Showing 5 changed files with 400 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div *ngIf="licenses.length > 0 || uris.length > 0" class="item-page-field">
<ng-container *ngIf="isCcLicense; then ccLicense; else default">
</ng-container>
<ng-template #ccLicense>
<ds-item-page-cc-license-field [item]="item" [variant]="'full'"></ds-item-page-cc-license-field>
</ng-template>
<ng-template #default>
<ds-metadata-field-wrapper [label]="'item.page.license.title' | translate">
<ng-container *ngIf="licenses.length == uris.length; then licensesWithLink; else listOfLicenses">
</ng-container>
<ng-template #licensesWithLink>
<ng-container *ngFor="let license of licenses; let last=last; let i=index;">
<a [href]="uris[i]" target="_blank" class="license-link"><span class="license-text">{{ license }}</span></a>
<span class="separator" *ngIf="!last">{{ separator }}</span>
</ng-container>
</ng-template>
<ng-template #listOfLicenses>
<ng-container *ngFor="let license of licenses; let last=last;">
<span class="license-text">{{ license }}</span>
<span class="separator" *ngIf="!last || uris.length > 0">{{ separator }}</span>
</ng-container>
<ng-container *ngFor="let uri of uris; let last=last;">
<a [href]="uri" target="_blank" class="license-link"><span class="license-text">{{ uri }}</span></a>
<span class="separator" *ngIf="!last">{{ separator }}</span>
</ng-container>
</ng-template>
</ds-metadata-field-wrapper>
</ng-template>
</div>
Original file line number Diff line number Diff line change
@@ -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<ItemPageLicenseFieldComponent>,
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<ItemPageLicenseFieldComponent>;

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])
);
}
},
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit c9ad7e3

Please sign in to comment.