forked from DSpace/dspace-angular
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ItemPageCcLicenseFieldComponent to render rights information
- Loading branch information
Showing
5 changed files
with
400 additions
and
0 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
...age/simple/field-components/specific-field/license/item-page-license-field.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
280 changes: 280 additions & 0 deletions
280
.../simple/field-components/specific-field/license/item-page-license-field.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
); | ||
} | ||
}, | ||
); | ||
}); | ||
}); | ||
}); |
85 changes: 85 additions & 0 deletions
85
...-page/simple/field-components/specific-field/license/item-page-license-field.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.