initial metadata service with full coverage

This commit is contained in:
William Welling
2017-10-13 00:38:26 -05:00
parent c37a30ec2a
commit 2f9c8468fd
10 changed files with 226 additions and 50 deletions

View File

@@ -6,7 +6,7 @@ import { HomePageComponent } from './home-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ path: '', component: HomePageComponent, pathMatch: 'full' } { path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'DSpace Angular :: Home' } }
]) ])
] ]
}) })

View File

@@ -6,7 +6,7 @@ import { SearchPageComponent } from './search-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ path: '', component: SearchPageComponent } { path: '', component: SearchPageComponent, data: { title: 'DSpace Angular :: Search' } }
]) ])
] ]
}) })

View File

@@ -1,13 +1,30 @@
import { inheritSerialization } from 'cerialize'; import { inheritSerialization, autoserialize } from 'cerialize';
import { mapsTo } from '../builders/build-decorators';
import { BitstreamFormat } from '../../shared/bitstream-format.model';
import { NormalizedObject } from './normalized-object.model'; import { NormalizedObject } from './normalized-object.model';
@mapsTo(BitstreamFormat)
@inheritSerialization(NormalizedObject) @inheritSerialization(NormalizedObject)
export class NormalizedBitstreamFormat extends NormalizedObject { export class NormalizedBitstreamFormat extends NormalizedObject {
// TODO: this class was created as a placeholder when we connected to the live rest api
get uuid(): string { @autoserialize
return this.self; shortDescription: string;
}
@autoserialize
description: string;
@autoserialize
mimetype: string;
@autoserialize
supportLevel: number;
@autoserialize
internal: boolean;
@autoserialize
extensions: string;
} }

View File

@@ -21,16 +21,11 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
@autoserialize @autoserialize
content: string; content: string;
/**
* The mime type of this Bitstream
*/
@autoserialize
mimetype: string;
/** /**
* The format of this Bitstream * The format of this Bitstream
*/ */
@autoserialize @autoserialize
@relationship(ResourceType.BitstreamFormat, false)
format: string; format: string;
/** /**

View File

@@ -15,11 +15,9 @@ export class NormalizedObjectFactory {
case ResourceType.Bitstream: { case ResourceType.Bitstream: {
return NormalizedBitstream return NormalizedBitstream
} }
// commented out for now, bitstreamformats aren't used in the UI yet case ResourceType.BitstreamFormat: {
// and slow things down noticeably return NormalizedBitstreamFormat
// case ResourceType.BitstreamFormat: { }
// return NormalizedBitstreamFormat
// }
case ResourceType.Bundle: { case ResourceType.Bundle: {
return NormalizedBundle return NormalizedBundle
} }

View File

@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, async, fakeAsync, inject, tick } from '@angu
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { Location, CommonModule } from '@angular/common'; import { Location, CommonModule } from '@angular/common';
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { Component, DebugElement, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { By, Meta, MetaDefinition } from '@angular/platform-browser'; import { By, Meta, MetaDefinition } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -58,6 +58,10 @@ describe('MetadataService', () => {
let router: Router; let router: Router;
let fixture: ComponentFixture<TestComponent>; let fixture: ComponentFixture<TestComponent>;
let tagStore: Map<string, MetaDefinition[]>;
let envConfig: GlobalConfig;
beforeEach(() => { beforeEach(() => {
store = new Store<CoreState>(undefined, undefined, undefined); store = new Store<CoreState>(undefined, undefined, undefined);
@@ -74,7 +78,7 @@ describe('MetadataService', () => {
StoreModule.forRoot({}), StoreModule.forRoot({}),
RouterTestingModule.withRoutes([ RouterTestingModule.withRoutes([
{ path: 'items/:id', component: DummyItemComponent, pathMatch: 'full', data: { type: NormalizedItem } }, { path: 'items/:id', component: DummyItemComponent, pathMatch: 'full', data: { type: NormalizedItem } },
{ path: 'other', component: DummyItemComponent, pathMatch: 'full' } { path: 'other', component: DummyItemComponent, pathMatch: 'full', data: { title: 'Dummy Title', description: 'This is a dummy component for testing!' } }
]) ])
], ],
declarations: [ declarations: [
@@ -95,11 +99,14 @@ describe('MetadataService', () => {
meta = TestBed.get(Meta); meta = TestBed.get(Meta);
metadataService = TestBed.get(MetadataService); metadataService = TestBed.get(MetadataService);
envConfig = TestBed.get(GLOBAL_CONFIG);
router = TestBed.get(Router); router = TestBed.get(Router);
location = TestBed.get(Location); location = TestBed.get(Location);
fixture = TestBed.createComponent(TestComponent); fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
tagStore = metadataService.getTagStore();
}); });
beforeEach(() => { beforeEach(() => {
@@ -112,40 +119,45 @@ describe('MetadataService', () => {
spyOn(remoteDataBuildService, 'build').and.returnValue(MockItem); spyOn(remoteDataBuildService, 'build').and.returnValue(MockItem);
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick(); tick();
const tagStore: Map<string, MetaDefinition[]> = metadataService.getTagStore();
expect(tagStore.get('citation_title').length).toEqual(1);
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document'); expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26T19:58:25Z');
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
expect(tagStore.get('citation_language')[0].content).toEqual('en');
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
})); }));
it('items page should set meta tags as published Thesis', fakeAsync(() => { it('items page should set meta tags as published Thesis', fakeAsync(() => {
spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Thesis'))); spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Thesis')));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick(); tick();
const tagStore: Map<string, MetaDefinition[]> = metadataService.getTagStore();
expect(tagStore.get('citation_dissertation_name').length).toEqual(1);
expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document'); expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document');
expect(tagStore.get('citation_dissertation_institution').length).toEqual(1);
expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher'); expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher');
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([envConfig.ui.baseUrl, router.url].join(''));
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content');
})); }));
it('items page should set meta tags as published Technical Report', fakeAsync(() => { it('items page should set meta tags as published Technical Report', fakeAsync(() => {
spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Technical Report'))); spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Technical Report')));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick(); tick();
const tagStore: Map<string, MetaDefinition[]> = metadataService.getTagStore();
expect(tagStore.get('citation_technical_report_institution').length).toEqual(1);
expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher'); expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher');
})); }));
it('other navigation should clear meta tags', fakeAsync(() => { it('other navigation should title and description', fakeAsync(() => {
spyOn(remoteDataBuildService, 'build').and.returnValue(MockItem);
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
expect(tagStore.size).toBeGreaterThan(0)
router.navigate(['/other']); router.navigate(['/other']);
tick(); tick();
const tagStore: Map<string, MetaDefinition[]> = metadataService.getTagStore(); expect(tagStore.size).toEqual(2);
expect(tagStore.size).toEqual(0); expect(tagStore.get('title')[0].content).toEqual('Dummy Title');
expect(tagStore.get('description')[0].content).toEqual('This is a dummy component for testing!');
})); }));
const mockType = (mockItem: Item, type: string): Item => { const mockType = (mockItem: Item, type: string): Item => {
const typedMockItem = Object.assign({}, mockItem) as Item; const typedMockItem = Object.assign(new Item(), mockItem) as Item;
for (const metadatum of typedMockItem.metadata) { for (const metadatum of typedMockItem.metadata) {
if (metadatum.key === 'dc.type') { if (metadatum.key === 'dc.type') {
metadatum.value = type; metadatum.value = type;
@@ -156,7 +168,7 @@ describe('MetadataService', () => {
} }
const mockPublisher = (mockItem: Item): Item => { const mockPublisher = (mockItem: Item): Item => {
const publishedMockItem = Object.assign({}, mockItem) as Item; const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
publishedMockItem.metadata.push({ publishedMockItem.metadata.push({
key: 'dc.publisher', key: 'dc.publisher',
language: 'en_US', language: 'en_US',

View File

@@ -40,15 +40,17 @@ export class MetadataService {
private meta: Meta, private meta: Meta,
@Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig @Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig
) { ) {
// TODO:
this.meta.addTags([ this.meta.addTags([
{ property: 'og:title', content: 'DSpace Angular Universal' } { property: 'og:title', content: 'DSpace Angular Universal' },
{ property: 'og:description', content: 'The modern front-end for DSpace 7.' }
]); ]);
this.initialized = false; this.initialized = false;
this.tagStore = new Map<string, MetaDefinition[]>(); this.tagStore = new Map<string, MetaDefinition[]>();
} }
public listenForRouteChange(): void { public listenForRouteChange(): void {
const subscription = this.router.events this.router.events
.filter((event) => event instanceof NavigationEnd) .filter((event) => event instanceof NavigationEnd)
.map(() => this.router.routerState.root) .map(() => this.router.routerState.root)
.map((route: ActivatedRoute) => { .map((route: ActivatedRoute) => {
@@ -60,6 +62,7 @@ export class MetadataService {
} }
private processRouteChange(routeInfo: any): void { private processRouteChange(routeInfo: any): void {
this.clearMetaTags();
if (routeInfo.params.value.id && routeInfo.data.value.type) { if (routeInfo.params.value.id && routeInfo.data.value.type) {
this.objectCacheService.getByUUID(routeInfo.params.value.id, routeInfo.data.value.type) this.objectCacheService.getByUUID(routeInfo.params.value.id, routeInfo.data.value.type)
.first().subscribe((normalizedObject: CacheableObject) => { .first().subscribe((normalizedObject: CacheableObject) => {
@@ -70,13 +73,18 @@ export class MetadataService {
this.currentObject.next(dspaceObject); this.currentObject.next(dspaceObject);
}); });
} else { } else {
this.clearMetaTags(); if (routeInfo.data.value.title) {
this.addMetaTag('title', routeInfo.data.value.title);
}
if (routeInfo.data.value.description) {
this.addMetaTag('description', routeInfo.data.value.description);
}
} }
} }
private initialize(dspaceObject: DSpaceObject): void { private initialize(dspaceObject: DSpaceObject): void {
this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject); this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject);
const subscription = this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => { this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => {
this.setMetaTags(); this.setMetaTags();
}); });
this.initialized = true; this.initialized = true;
@@ -91,7 +99,8 @@ export class MetadataService {
private setMetaTags(): void { private setMetaTags(): void {
this.clearMetaTags(); this.setTitleTag();
this.setDescriptionTag();
this.setCitationTitleTag(); this.setCitationTitleTag();
this.setCitationAuthorTags(); this.setCitationAuthorTags();
@@ -131,6 +140,23 @@ export class MetadataService {
} }
/**
* Add <meta name="title" ... > to the <head>
*/
private setTitleTag(): void {
const value = this.getMetaTagValue('dc.title');
this.addMetaTag('title', value);
}
/**
* Add <meta name="description" ... > to the <head>
*/
private setDescriptionTag(): void {
// TODO: truncate abstract
const value = this.getMetaTagValue('dc.description.abstract');
this.addMetaTag('desciption', value);
}
/** /**
* Add <meta name="citation_title" ... > to the <head> * Add <meta name="citation_title" ... > to the <head>
*/ */
@@ -175,7 +201,7 @@ export class MetadataService {
* Add <meta name="citation_language" ... > to the <head> * Add <meta name="citation_language" ... > to the <head>
*/ */
private setCitationLanguageTag(): void { private setCitationLanguageTag(): void {
const value = this.getMetaTagValue('dc.language.iso'); const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']);
this.addMetaTag('citation_language', value); this.addMetaTag('citation_language', value);
} }
@@ -229,12 +255,13 @@ export class MetadataService {
const item = this.currentObject.value as Item; const item = this.currentObject.value as Item;
// NOTE: Observable resolves many times with same data // NOTE: Observable resolves many times with same data
// taking only two, fist one is empty array // taking only two, fist one is empty array
const subscription = item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => { item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => {
for (const bitstream of bitstreams) { for (const bitstream of bitstreams) {
if (bitstream.mimetype === 'application/pdf') { bitstream.format.payload.take(1).subscribe((format) => {
this.addMetaTag('citation_abstract_html_url', bitstream.content); if (format.mimetype === 'application/pdf') {
break; this.addMetaTag('citation_pdf_url', bitstream.content);
} }
});
} }
}); });
} }

View File

@@ -0,0 +1,17 @@
import { DSpaceObject } from './dspace-object.model';
export class BitstreamFormat extends DSpaceObject {
shortDescription: string;
description: string;
mimetype: string;
supportLevel: number;
internal: boolean;
extensions: string;
}

View File

@@ -1,6 +1,7 @@
import { DSpaceObject } from './dspace-object.model'; import { DSpaceObject } from './dspace-object.model';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { Item } from './item.model'; import { Item } from './item.model';
import { BitstreamFormat } from './bitstream-format.model';
export class Bitstream extends DSpaceObject { export class Bitstream extends DSpaceObject {
@@ -9,11 +10,6 @@ export class Bitstream extends DSpaceObject {
*/ */
sizeBytes: number; sizeBytes: number;
/**
* The mime type of this Bitstream
*/
mimetype: string;
/** /**
* The description of this Bitstream * The description of this Bitstream
*/ */
@@ -24,6 +20,11 @@ export class Bitstream extends DSpaceObject {
*/ */
bundleName: string; bundleName: string;
/**
* An array of Bitstream Format of this Bitstream
*/
format: RemoteData<BitstreamFormat>;
/** /**
* An array of Items that are direct parents of this Bitstream * An array of Items that are direct parents of this Bitstream
*/ */

View File

@@ -2,6 +2,7 @@ import { Observable } from 'rxjs/Observable';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
/* tslint:disable:no-shadowed-variable */
export const MockItem: Item = Object.assign(new Item(), { export const MockItem: Item = Object.assign(new Item(), {
handle: '10673/6', handle: '10673/6',
lastModified: '2017-04-24T19:44:08.178+0000', lastModified: '2017-04-24T19:44:08.178+0000',
@@ -33,7 +34,114 @@ export const MockItem: Item = Object.assign(new Item(), {
observer.next({}); observer.next({});
}), }),
payload: Observable.create((observer) => { payload: Observable.create((observer) => {
observer.next([]); observer.next([
{
sizeBytes: 10201,
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10',
scheduler: null
},
requestPending: Observable.create((observer) => {
observer.next(false);
}),
responsePending: Observable.create((observer) => {
observer.next(false);
}),
isSuccessFul: Observable.create((observer) => {
observer.next(true);
}),
errorMessage: Observable.create((observer) => {
observer.next('');
}),
statusCode: Observable.create((observer) => {
observer.next(202);
}),
pageInfo: Observable.create((observer) => {
observer.next({});
}),
payload: Observable.create((observer) => {
observer.next({
shortDescription: 'Microsoft Word XML',
description: 'Microsoft Word XML',
mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
supportLevel: 0,
internal: false,
extensions: null,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10'
});
})
},
bundleName: 'ORIGINAL',
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
name: 'test_word.docx',
metadata: [
{
key: 'dc.title',
language: null,
value: 'test_word.docx'
}
]
},
{
sizeBytes: 31302,
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content',
format: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4',
scheduler: null
},
requestPending: Observable.create((observer) => {
observer.next(false);
}),
responsePending: Observable.create((observer) => {
observer.next(false);
}),
isSuccessFul: Observable.create((observer) => {
observer.next(true);
}),
errorMessage: Observable.create((observer) => {
observer.next('');
}),
statusCode: Observable.create((observer) => {
observer.next(202);
}),
pageInfo: Observable.create((observer) => {
observer.next({});
}),
payload: Observable.create((observer) => {
observer.next({
shortDescription: 'Adobe PDF',
description: 'Adobe Portable Document Format',
mimetype: 'application/pdf',
supportLevel: 0,
internal: false,
extensions: null,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4'
});
})
},
bundleName: 'ORIGINAL',
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28',
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
type: 'bitstream',
name: 'test_pdf.pdf',
metadata: [
{
key: 'dc.title',
language: null,
value: 'test_pdf.pdf'
}
]
}
]);
}) })
}, },
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
@@ -162,3 +270,4 @@ export const MockItem: Item = Object.assign(new Item(), {
}) })
} }
}) })
/* tslint:enable:no-shadowed-variable */