74647: Generator meta

This commit is contained in:
Kristof De Langhe
2020-12-07 16:42:33 +01:00
parent 32a29c4a17
commit be05647e5d
8 changed files with 209 additions and 2 deletions

View File

@@ -172,6 +172,8 @@ import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agree
import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service';
import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard';
import { UsageReport } from './statistics/models/usage-report.model'; import { UsageReport } from './statistics/models/usage-report.model';
import { RootDataService } from './data/root-data.service';
import { Root } from './data/root.model';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -302,6 +304,7 @@ const PROVIDERS = [
EndUserAgreementCurrentUserGuard, EndUserAgreementCurrentUserGuard,
EndUserAgreementCookieGuard, EndUserAgreementCookieGuard,
EndUserAgreementService, EndUserAgreementService,
RootDataService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
@@ -374,6 +377,7 @@ export const models =
VocabularyEntryDetail, VocabularyEntryDetail,
ConfigurationProperty, ConfigurationProperty,
UsageReport, UsageReport,
Root,
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,38 @@
import { RootDataService } from './root-data.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
import { Root } from './root.model';
describe('RootDataService', () => {
let service: RootDataService;
let halService: HALEndpointService;
let rootEndpoint;
beforeEach(() => {
rootEndpoint = 'root-endpoint';
halService = jasmine.createSpyObj('halService', {
getRootHref: rootEndpoint
});
service = new RootDataService(null, null, null, null, halService, null, null, null);
(service as any).dataService = jasmine.createSpyObj('dataService', {
findByHref: createSuccessfulRemoteDataObject$({})
})
});
describe('findRoot', () => {
let result$: Observable<RemoteData<Root>>;
beforeEach(() => {
result$ = service.findRoot();
});
it('should call findByHref using the root endpoint', (done) => {
result$.subscribe(() => {
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(rootEndpoint);
done();
});
});
});
});

View File

@@ -0,0 +1,72 @@
import { DataService } from './data.service';
import { Root } from './root.model';
import { Injectable } from '@angular/core';
import { ROOT } from './root.resource-type';
import { dataService } from '../cache/builders/build-decorators';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/* tslint:disable:max-classes-per-file */
/**
* A private DataService implementation to delegate specific methods to.
*/
class DataServiceImpl extends DataService<Root> {
protected linkPath = '';
protected responseMsToLive = 6 * 60 * 60 * 1000;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Root>) {
super();
}
}
/**
* A service to retrieve the {@link Root} object from the REST API.
*/
@Injectable()
@dataService(ROOT)
export class RootDataService {
/**
* A private DataService instance to delegate specific methods to.
*/
private dataService: DataServiceImpl;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Root>) {
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
}
/**
* Find the {@link Root} object of the REST API
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
findRoot(...linksToFollow: Array<FollowLinkConfig<Root>>): Observable<RemoteData<Root>> {
return this.dataService.findByHref(this.halService.getRootHref(), ...linksToFollow);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,55 @@
import { typedObject } from '../cache/builders/build-decorators';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ROOT } from './root.resource-type';
import { excludeFromEquals } from '../utilities/equals.decorators';
import { autoserialize, deserialize } from 'cerialize';
import { ResourceType } from '../shared/resource-type';
import { HALLink } from '../shared/hal-link.model';
/**
* The root rest api resource
*/
@typedObject
export class Root implements CacheableObject {
static type = ROOT;
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
/**
* The url for the dspace UI
*/
@autoserialize
dspaceUI: string;
/**
* The repository Name
*/
@autoserialize
dspaceName: string;
/**
* The url for the rest api
*/
@autoserialize
dspaceServer: string;
/**
* The current DSpace version
*/
@autoserialize
dspaceVersion: string;
/**
* The {@link HALLink}s for the root object
*/
@deserialize
_links: {
self: HALLink;
[k: string]: HALLink | HALLink[];
}
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../shared/resource-type';
/**
* The resource type for the root endpoint
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const ROOT = new ResourceType('root');

View File

@@ -54,6 +54,8 @@ import { environment } from '../../../environments/environment';
import { storeModuleConfig } from '../../app.reducer'; import { storeModuleConfig } from '../../app.reducer';
import { HardRedirectService } from '../services/hard-redirect.service'; import { HardRedirectService } from '../services/hard-redirect.service';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { RootDataService } from '../data/root-data.service';
import { Root } from '../data/root.model';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@Component({ @Component({
@@ -92,6 +94,7 @@ describe('MetadataService', () => {
let remoteDataBuildService: RemoteDataBuildService; let remoteDataBuildService: RemoteDataBuildService;
let itemDataService: ItemDataService; let itemDataService: ItemDataService;
let authService: AuthService; let authService: AuthService;
let rootService: RootDataService;
let location: Location; let location: Location;
let router: Router; let router: Router;
@@ -131,6 +134,11 @@ describe('MetadataService', () => {
} }
} }
}; };
rootService = jasmine.createSpyObj('rootService', {
findRoot: createSuccessfulRemoteDataObject$(Object.assign(new Root(), {
dspaceVersion: 'mock-dspace-version'
}))
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -169,6 +177,7 @@ describe('MetadataService', () => {
{ provide: DefaultChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService }, { provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: RootDataService, useValue: rootService },
Meta, Meta,
Title, Title,
// tslint:disable-next-line:no-empty // tslint:disable-next-line:no-empty
@@ -223,6 +232,13 @@ describe('MetadataService', () => {
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('items page should set meta tag for Generator containing the current DSpace version', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version');
}));
it('other navigation should title and description', fakeAsync(() => { it('other navigation should title and description', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);

View File

@@ -22,6 +22,7 @@ import { Item } from '../shared/item.model';
import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators'; import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators';
import { HardRedirectService } from '../services/hard-redirect.service'; import { HardRedirectService } from '../services/hard-redirect.service';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { RootDataService } from '../data/root-data.service';
@Injectable() @Injectable()
export class MetadataService { export class MetadataService {
@@ -40,7 +41,8 @@ export class MetadataService {
private dsoNameService: DSONameService, private dsoNameService: DSONameService,
private bitstreamDataService: BitstreamDataService, private bitstreamDataService: BitstreamDataService,
private bitstreamFormatDataService: BitstreamFormatDataService, private bitstreamFormatDataService: BitstreamFormatDataService,
private redirectService: HardRedirectService private redirectService: HardRedirectService,
private rootService: RootDataService
) { ) {
// TODO: determine what open graph meta tags are needed and whether // TODO: determine what open graph meta tags are needed and whether
// the differ per route. potentially add image based on DSpaceObject // the differ per route. potentially add image based on DSpaceObject
@@ -136,6 +138,8 @@ export class MetadataService {
this.setCitationTechReportInstitutionTag(); this.setCitationTechReportInstitutionTag();
} }
this.setGenerator();
// this.setCitationJournalTitleTag(); // this.setCitationJournalTitleTag();
// this.setCitationVolumeTag(); // this.setCitationVolumeTag();
// this.setCitationIssueTag(); // this.setCitationIssueTag();
@@ -290,6 +294,15 @@ export class MetadataService {
} }
} }
/**
* Add <meta name="Generator" ... > to the <head> containing the current DSpace version
*/
private setGenerator(): void {
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
this.addMetaTag('Generator', root.dspaceVersion);
});
}
private hasType(value: string): boolean { private hasType(value: string): boolean {
return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true }); return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true });
} }

View File

@@ -15,7 +15,7 @@ export class HALEndpointService {
constructor(private requestService: RequestService) { constructor(private requestService: RequestService) {
} }
protected getRootHref(): string { public getRootHref(): string {
return new RESTURLCombiner('/api').toString(); return new RESTURLCombiner('/api').toString();
} }