From be05647e5d32c46524bb72a17798acc0e997b66b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 7 Dec 2020 16:42:33 +0100 Subject: [PATCH 1/5] 74647: Generator meta --- src/app/core/core.module.ts | 4 ++ src/app/core/data/root-data.service.spec.ts | 38 ++++++++++ src/app/core/data/root-data.service.ts | 72 +++++++++++++++++++ src/app/core/data/root.model.ts | 55 ++++++++++++++ src/app/core/data/root.resource-type.ts | 9 +++ .../core/metadata/metadata.service.spec.ts | 16 +++++ src/app/core/metadata/metadata.service.ts | 15 +++- src/app/core/shared/hal-endpoint.service.ts | 2 +- 8 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/app/core/data/root-data.service.spec.ts create mode 100644 src/app/core/data/root-data.service.ts create mode 100644 src/app/core/data/root.model.ts create mode 100644 src/app/core/data/root.resource-type.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 2203377603..43c71533ab 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -172,6 +172,8 @@ import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agree import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; 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 @@ -302,6 +304,7 @@ const PROVIDERS = [ EndUserAgreementCurrentUserGuard, EndUserAgreementCookieGuard, EndUserAgreementService, + RootDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -374,6 +377,7 @@ export const models = VocabularyEntryDetail, ConfigurationProperty, UsageReport, + Root, ]; @NgModule({ diff --git a/src/app/core/data/root-data.service.spec.ts b/src/app/core/data/root-data.service.spec.ts new file mode 100644 index 0000000000..baa17b846b --- /dev/null +++ b/src/app/core/data/root-data.service.spec.ts @@ -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>; + + beforeEach(() => { + result$ = service.findRoot(); + }); + + it('should call findByHref using the root endpoint', (done) => { + result$.subscribe(() => { + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(rootEndpoint); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts new file mode 100644 index 0000000000..0b9ac1c145 --- /dev/null +++ b/src/app/core/data/root-data.service.ts @@ -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 { + protected linkPath = ''; + protected responseMsToLive = 6 * 60 * 60 * 1000; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + 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, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + 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>): Observable> { + return this.dataService.findByHref(this.halService.getRootHref(), ...linksToFollow); + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/root.model.ts b/src/app/core/data/root.model.ts new file mode 100644 index 0000000000..abfd25d34d --- /dev/null +++ b/src/app/core/data/root.model.ts @@ -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[]; + } +} diff --git a/src/app/core/data/root.resource-type.ts b/src/app/core/data/root.resource-type.ts new file mode 100644 index 0000000000..6f94e2ceaf --- /dev/null +++ b/src/app/core/data/root.resource-type.ts @@ -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'); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 28fe8e1acc..f961f22560 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -54,6 +54,8 @@ import { environment } from '../../../environments/environment'; import { storeModuleConfig } from '../../app.reducer'; import { HardRedirectService } from '../services/hard-redirect.service'; 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 */ @Component({ @@ -92,6 +94,7 @@ describe('MetadataService', () => { let remoteDataBuildService: RemoteDataBuildService; let itemDataService: ItemDataService; let authService: AuthService; + let rootService: RootDataService; let location: Location; 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({ imports: [ @@ -169,6 +177,7 @@ describe('MetadataService', () => { { provide: DefaultChangeAnalyzer, useValue: {} }, { provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: RootDataService, useValue: rootService }, Meta, Title, // tslint:disable-next-line:no-empty @@ -223,6 +232,13 @@ describe('MetadataService', () => { 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(() => { spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 90171bac10..c44c4fbb15 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -22,6 +22,7 @@ import { Item } from '../shared/item.model'; import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators'; import { HardRedirectService } from '../services/hard-redirect.service'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { RootDataService } from '../data/root-data.service'; @Injectable() export class MetadataService { @@ -40,7 +41,8 @@ export class MetadataService { private dsoNameService: DSONameService, private bitstreamDataService: BitstreamDataService, private bitstreamFormatDataService: BitstreamFormatDataService, - private redirectService: HardRedirectService + private redirectService: HardRedirectService, + private rootService: RootDataService ) { // TODO: determine what open graph meta tags are needed and whether // the differ per route. potentially add image based on DSpaceObject @@ -136,6 +138,8 @@ export class MetadataService { this.setCitationTechReportInstitutionTag(); } + this.setGenerator(); + // this.setCitationJournalTitleTag(); // this.setCitationVolumeTag(); // this.setCitationIssueTag(); @@ -290,6 +294,15 @@ export class MetadataService { } } + /** + * Add to the 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 { return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true }); } diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index a74650ce5a..219ea453bf 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -15,7 +15,7 @@ export class HALEndpointService { constructor(private requestService: RequestService) { } - protected getRootHref(): string { + public getRootHref(): string { return new RESTURLCombiner('/api').toString(); } From f7f784157dfd337cb8ac1bb87589fdb505532b50 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 8 Dec 2020 14:51:40 +0100 Subject: [PATCH 2/5] 74647: Add Generator meta tag on all pages --- src/app/core/metadata/metadata.service.spec.ts | 12 +++--------- src/app/core/metadata/metadata.service.ts | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index f961f22560..2952e665d3 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -232,24 +232,18 @@ describe('MetadataService', () => { 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 add title, description and Generator', fakeAsync(() => { spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); expect(tagStore.size).toBeGreaterThan(0); router.navigate(['/other']); tick(); - expect(tagStore.size).toEqual(2); + expect(tagStore.size).toEqual(3); expect(title.getTitle()).toEqual('Dummy Title'); expect(tagStore.get('title')[0].content).toEqual('Dummy Title'); expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!'); + expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version'); })); describe('when the item has no bitstreams', () => { diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index c44c4fbb15..414fb13a88 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -93,6 +93,8 @@ export class MetadataService { this.addMetaTag('description', translatedDescription); }); } + + this.setGenerator(); } private initialize(dspaceObject: DSpaceObject): void { @@ -138,8 +140,6 @@ export class MetadataService { this.setCitationTechReportInstitutionTag(); } - this.setGenerator(); - // this.setCitationJournalTitleTag(); // this.setCitationVolumeTag(); // this.setCitationIssueTag(); From d70685a5f2f9ac9390294b6fb94450cecba266a8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 24 Dec 2020 13:39:37 +0100 Subject: [PATCH 3/5] 74647: findByHref and findAllByHref for RootDataService --- src/app/core/data/root-data.service.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts index 0b9ac1c145..20949daf50 100644 --- a/src/app/core/data/root-data.service.ts +++ b/src/app/core/data/root-data.service.ts @@ -15,6 +15,8 @@ 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'; +import { FindListOptions } from './request.models'; +import { PaginatedList } from './paginated-list'; /* tslint:disable:max-classes-per-file */ @@ -68,5 +70,26 @@ export class RootDataService { findRoot(...linksToFollow: Array>): Observable> { return this.dataService.findByHref(this.halService.getRootHref(), ...linksToFollow); } + + /** + * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param findListOptions Find list options object + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + } } /* tslint:enable:max-classes-per-file */ From a06599ef3f4da97d9e3da166b98f23fbb698d10b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 24 Dec 2020 14:06:20 +0100 Subject: [PATCH 4/5] 74647: Test case fix --- src/app/core/data/root-data.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/root-data.service.spec.ts b/src/app/core/data/root-data.service.spec.ts index baa17b846b..06f2f92dbd 100644 --- a/src/app/core/data/root-data.service.spec.ts +++ b/src/app/core/data/root-data.service.spec.ts @@ -30,7 +30,7 @@ describe('RootDataService', () => { it('should call findByHref using the root endpoint', (done) => { result$.subscribe(() => { - expect((service as any).dataService.findByHref).toHaveBeenCalledWith(rootEndpoint); + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(rootEndpoint, true); done(); }); }); From 9a0e2bf99603464b8d9be6011379d1b6e84ff801 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Feb 2021 17:11:11 +0100 Subject: [PATCH 5/5] 74647: Fixed tslint issues --- src/app/core/data/root-data.service.spec.ts | 2 +- src/app/core/data/root-data.service.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/root-data.service.spec.ts b/src/app/core/data/root-data.service.spec.ts index 06f2f92dbd..80a180e463 100644 --- a/src/app/core/data/root-data.service.spec.ts +++ b/src/app/core/data/root-data.service.spec.ts @@ -18,7 +18,7 @@ describe('RootDataService', () => { service = new RootDataService(null, null, null, null, halService, null, null, null); (service as any).dataService = jasmine.createSpyObj('dataService', { findByHref: createSuccessfulRemoteDataObject$({}) - }) + }); }); describe('findRoot', () => { diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts index e77e670fac..cd55aa7f7c 100644 --- a/src/app/core/data/root-data.service.ts +++ b/src/app/core/data/root-data.service.ts @@ -69,7 +69,7 @@ export class RootDataService { * the response becomes stale * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - findRoot(reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + findRoot(reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { return this.dataService.findByHref(this.halService.getRootHref(), reRequestOnStale, ...linksToFollow); } @@ -82,7 +82,7 @@ export class RootDataService { * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * should be automatically resolved */ - findByHref(href: string, reRequestOnStale = true, ...linksToFollow: Array>): Observable> { + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); } @@ -96,7 +96,7 @@ export class RootDataService { * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s * should be automatically resolved */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: Array>): Observable>> { + findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); } }