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({
imports: [
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({
imports: [
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';
@mapsTo(BitstreamFormat)
@inheritSerialization(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 {
return this.self;
}
@autoserialize
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
content: string;
/**
* The mime type of this Bitstream
*/
@autoserialize
mimetype: string;
/**
* The format of this Bitstream
*/
@autoserialize
@relationship(ResourceType.BitstreamFormat, false)
format: string;
/**

View File

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

View File

@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, async, fakeAsync, inject, tick } from '@angu
import { RouterTestingModule } from '@angular/router/testing';
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 { Router } from '@angular/router';
@@ -58,6 +58,10 @@ describe('MetadataService', () => {
let router: Router;
let fixture: ComponentFixture<TestComponent>;
let tagStore: Map<string, MetaDefinition[]>;
let envConfig: GlobalConfig;
beforeEach(() => {
store = new Store<CoreState>(undefined, undefined, undefined);
@@ -74,7 +78,7 @@ describe('MetadataService', () => {
StoreModule.forRoot({}),
RouterTestingModule.withRoutes([
{ 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: [
@@ -95,11 +99,14 @@ describe('MetadataService', () => {
meta = TestBed.get(Meta);
metadataService = TestBed.get(MetadataService);
envConfig = TestBed.get(GLOBAL_CONFIG);
router = TestBed.get(Router);
location = TestBed.get(Location);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
tagStore = metadataService.getTagStore();
});
beforeEach(() => {
@@ -112,40 +119,45 @@ describe('MetadataService', () => {
spyOn(remoteDataBuildService, 'build').and.returnValue(MockItem);
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
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_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(() => {
spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Thesis')));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
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_institution').length).toEqual(1);
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(() => {
spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Technical Report')));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
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');
}));
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']);
tick();
const tagStore: Map<string, MetaDefinition[]> = metadataService.getTagStore();
expect(tagStore.size).toEqual(0);
expect(tagStore.size).toEqual(2);
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 typedMockItem = Object.assign({}, mockItem) as Item;
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
for (const metadatum of typedMockItem.metadata) {
if (metadatum.key === 'dc.type') {
metadatum.value = type;
@@ -156,7 +168,7 @@ describe('MetadataService', () => {
}
const mockPublisher = (mockItem: Item): Item => {
const publishedMockItem = Object.assign({}, mockItem) as Item;
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
publishedMockItem.metadata.push({
key: 'dc.publisher',
language: 'en_US',

View File

@@ -40,15 +40,17 @@ export class MetadataService {
private meta: Meta,
@Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig
) {
// TODO:
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.tagStore = new Map<string, MetaDefinition[]>();
}
public listenForRouteChange(): void {
const subscription = this.router.events
this.router.events
.filter((event) => event instanceof NavigationEnd)
.map(() => this.router.routerState.root)
.map((route: ActivatedRoute) => {
@@ -60,6 +62,7 @@ export class MetadataService {
}
private processRouteChange(routeInfo: any): void {
this.clearMetaTags();
if (routeInfo.params.value.id && routeInfo.data.value.type) {
this.objectCacheService.getByUUID(routeInfo.params.value.id, routeInfo.data.value.type)
.first().subscribe((normalizedObject: CacheableObject) => {
@@ -70,13 +73,18 @@ export class MetadataService {
this.currentObject.next(dspaceObject);
});
} 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 {
this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject);
const subscription = this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => {
this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => {
this.setMetaTags();
});
this.initialized = true;
@@ -91,7 +99,8 @@ export class MetadataService {
private setMetaTags(): void {
this.clearMetaTags();
this.setTitleTag();
this.setDescriptionTag();
this.setCitationTitleTag();
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>
*/
@@ -175,7 +201,7 @@ export class MetadataService {
* Add <meta name="citation_language" ... > to the <head>
*/
private setCitationLanguageTag(): void {
const value = this.getMetaTagValue('dc.language.iso');
const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']);
this.addMetaTag('citation_language', value);
}
@@ -229,12 +255,13 @@ export class MetadataService {
const item = this.currentObject.value as Item;
// NOTE: Observable resolves many times with same data
// 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) {
if (bitstream.mimetype === 'application/pdf') {
this.addMetaTag('citation_abstract_html_url', bitstream.content);
break;
bitstream.format.payload.take(1).subscribe((format) => {
if (format.mimetype === 'application/pdf') {
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 { RemoteData } from '../data/remote-data';
import { Item } from './item.model';
import { BitstreamFormat } from './bitstream-format.model';
export class Bitstream extends DSpaceObject {
@@ -9,11 +10,6 @@ export class Bitstream extends DSpaceObject {
*/
sizeBytes: number;
/**
* The mime type of this Bitstream
*/
mimetype: string;
/**
* The description of this Bitstream
*/
@@ -24,6 +20,11 @@ export class Bitstream extends DSpaceObject {
*/
bundleName: string;
/**
* An array of Bitstream Format of this Bitstream
*/
format: RemoteData<BitstreamFormat>;
/**
* 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';
/* tslint:disable:no-shadowed-variable */
export const MockItem: Item = Object.assign(new Item(), {
handle: '10673/6',
lastModified: '2017-04-24T19:44:08.178+0000',
@@ -33,7 +34,114 @@ export const MockItem: Item = Object.assign(new Item(), {
observer.next({});
}),
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',
@@ -162,3 +270,4 @@ export const MockItem: Item = Object.assign(new Item(), {
})
}
})
/* tslint:enable:no-shadowed-variable */