mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
@@ -173,6 +173,11 @@ import { LinkHeadService } from './services/link-head.service';
|
|||||||
import { ResearcherProfileService } from './profile/researcher-profile.service';
|
import { ResearcherProfileService } from './profile/researcher-profile.service';
|
||||||
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
|
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
|
||||||
import { ResearcherProfile } from './profile/model/researcher-profile.model';
|
import { ResearcherProfile } from './profile/model/researcher-profile.model';
|
||||||
|
import { OrcidQueueService } from './orcid/orcid-queue.service';
|
||||||
|
import { OrcidHistoryDataService } from './orcid/orcid-history-data.service';
|
||||||
|
import { OrcidQueue } from './orcid/model/orcid-queue.model';
|
||||||
|
import { OrcidHistory } from './orcid/model/orcid-history.model';
|
||||||
|
import { OrcidAuthService } from './orcid/orcid-auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -300,7 +305,10 @@ const PROVIDERS = [
|
|||||||
GroupDataService,
|
GroupDataService,
|
||||||
FeedbackDataService,
|
FeedbackDataService,
|
||||||
ResearcherProfileService,
|
ResearcherProfileService,
|
||||||
ProfileClaimService
|
ProfileClaimService,
|
||||||
|
OrcidAuthService,
|
||||||
|
OrcidQueueService,
|
||||||
|
OrcidHistoryDataService,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,7 +370,10 @@ export const models =
|
|||||||
SearchConfig,
|
SearchConfig,
|
||||||
SubmissionAccessesModel,
|
SubmissionAccessesModel,
|
||||||
AccessStatusObject,
|
AccessStatusObject,
|
||||||
ResearcherProfile
|
ResearcherProfile,
|
||||||
|
OrcidQueue,
|
||||||
|
OrcidHistory,
|
||||||
|
AccessStatusObject
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
89
src/app/core/orcid/model/orcid-history.model.ts
Normal file
89
src/app/core/orcid/model/orcid-history.model.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ORCID_HISTORY } from './orcid-history.resource-type';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class the represents a Orcid History.
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class OrcidHistory extends CacheableObject {
|
||||||
|
|
||||||
|
static type = ORCID_HISTORY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this Orcid History record
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the related entity
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the profileItem of this Orcid History record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
profileItemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the entity related to this Orcid History record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the entity related to this Orcid History record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response status coming from ORCID api.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The putCode assigned by ORCID to the entity.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
putCode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last send attempt timestamp.
|
||||||
|
*/
|
||||||
|
lastAttempt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The success send attempt timestamp.
|
||||||
|
*/
|
||||||
|
successAttempt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response coming from ORCID.
|
||||||
|
*/
|
||||||
|
responseMessage: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Orcid History record
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
9
src/app/core/orcid/model/orcid-history.resource-type.ts
Normal file
9
src/app/core/orcid/model/orcid-history.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for OrcidHistory
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const ORCID_HISTORY = new ResourceType('orcidhistory');
|
68
src/app/core/orcid/model/orcid-queue.model.ts
Normal file
68
src/app/core/orcid/model/orcid-queue.model.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ORCID_QUEUE } from './orcid-queue.resource-type';
|
||||||
|
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class the represents a Orcid Queue.
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class OrcidQueue extends CacheableObject {
|
||||||
|
|
||||||
|
static type = ORCID_QUEUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this Orcid Queue record
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The record description.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the profileItem of this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
profileItemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the entity related to this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
entityId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
recordType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The operation related to this Orcid Queue record.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
operation: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Orcid Queue record
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
9
src/app/core/orcid/model/orcid-queue.resource-type.ts
Normal file
9
src/app/core/orcid/model/orcid-queue.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for OrcidQueue
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const ORCID_QUEUE = new ResourceType('orcidqueue');
|
329
src/app/core/orcid/orcid-auth.service.spec.ts
Normal file
329
src/app/core/orcid/orcid-auth.service.spec.ts
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
|
import { ResearcherProfile } from '../profile/model/researcher-profile.model';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { AddOperation, RemoveOperation } from 'fast-json-patch';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { NativeWindowRefMock } from '../../shared/mocks/mock-native-window-ref';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { OrcidAuthService } from './orcid-auth.service';
|
||||||
|
import { ResearcherProfileService } from '../profile/researcher-profile.service';
|
||||||
|
|
||||||
|
describe('OrcidAuthService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: OrcidAuthService;
|
||||||
|
let serviceAsAny: any;
|
||||||
|
|
||||||
|
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||||
|
let configurationDataService: ConfigurationDataService;
|
||||||
|
let nativeWindowService: NativeWindowRefMock;
|
||||||
|
let routerStub: any;
|
||||||
|
|
||||||
|
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
|
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
|
|
||||||
|
const researcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: researcherProfileId,
|
||||||
|
visible: false,
|
||||||
|
type: 'profile',
|
||||||
|
_links: {
|
||||||
|
item: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const researcherProfilePatched: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: researcherProfileId,
|
||||||
|
visible: true,
|
||||||
|
type: 'profile',
|
||||||
|
_links: {
|
||||||
|
item: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
id: 'mockItemUnlinkedToOrcid',
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'researcher-profile-id',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'researcher-profile-id',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.authenticated': [{
|
||||||
|
'value': '2022-06-10T15:15:12.952872',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.scope': [{
|
||||||
|
'value': '/authenticate',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}, {
|
||||||
|
'value': '/read-limited',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 1
|
||||||
|
}, {
|
||||||
|
'value': '/activities/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 2
|
||||||
|
}, {
|
||||||
|
'value': '/person/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 3
|
||||||
|
}],
|
||||||
|
'person.identifier.orcid': [{
|
||||||
|
'value': 'orcid-id',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnectionAllowAdmin = {
|
||||||
|
uuid: 'orcid.disconnection.allowed-users',
|
||||||
|
name: 'orcid.disconnection.allowed-users',
|
||||||
|
values: ['only_admin']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
const disconnectionAllowAdminOwner = {
|
||||||
|
uuid: 'orcid.disconnection.allowed-users',
|
||||||
|
name: 'orcid.disconnection.allowed-users',
|
||||||
|
values: ['admin_and_owner']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
const authorizeUrl = {
|
||||||
|
uuid: 'orcid.authorize-url',
|
||||||
|
name: 'orcid.authorize-url',
|
||||||
|
values: ['orcid.authorize-url']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const appClientId = {
|
||||||
|
uuid: 'orcid.application-client-id',
|
||||||
|
name: 'orcid.application-client-id',
|
||||||
|
values: ['orcid.application-client-id']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const orcidScope = {
|
||||||
|
uuid: 'orcid.scope',
|
||||||
|
name: 'orcid.scope',
|
||||||
|
values: ['/authenticate', '/read-limited']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
routerStub = new RouterMock();
|
||||||
|
researcherProfileService = jasmine.createSpyObj('ResearcherProfileService', {
|
||||||
|
findById: jasmine.createSpy('findById'),
|
||||||
|
updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations')
|
||||||
|
});
|
||||||
|
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
|
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||||
|
});
|
||||||
|
nativeWindowService = new NativeWindowRefMock();
|
||||||
|
|
||||||
|
service = new OrcidAuthService(
|
||||||
|
nativeWindowService,
|
||||||
|
configurationDataService,
|
||||||
|
researcherProfileService,
|
||||||
|
routerStub);
|
||||||
|
|
||||||
|
serviceAsAny = service;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('isLinkedToOrcid', () => {
|
||||||
|
it('should return true when item has metadata', () => {
|
||||||
|
const result = service.isLinkedToOrcid(mockItemLinkedToOrcid);
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when item has no metadata', () => {
|
||||||
|
const result = service.isLinkedToOrcid(mockItemUnlinkedToOrcid);
|
||||||
|
expect(result).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onlyAdminCanDisconnectProfileFromOrcid', () => {
|
||||||
|
it('should return true when property is only_admin', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdmin));
|
||||||
|
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: true
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on faild', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: false
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ownerCanDisconnectProfileFromOrcid', () => {
|
||||||
|
it('should return true when property is admin_and_owner', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdminOwner));
|
||||||
|
const result = service.ownerCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: true
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on faild', () => {
|
||||||
|
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
const result = service.ownerCanDisconnectProfileFromOrcid();
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: false
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('linkOrcidByItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
|
researcherProfileService.findById.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call updateByOrcidOperations method properly', () => {
|
||||||
|
const operations: AddOperation<string>[] = [{
|
||||||
|
path: '/orcid',
|
||||||
|
op: 'add',
|
||||||
|
value: 'test-code'
|
||||||
|
}];
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.linkOrcidByItem(mockItemUnlinkedToOrcid, 'test-code').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(researcherProfile, operations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unlinkOrcidByItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||||
|
researcherProfileService.findById.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call updateByOrcidOperations method properly', () => {
|
||||||
|
const operations: RemoveOperation[] = [{
|
||||||
|
path: '/orcid',
|
||||||
|
op: 'remove'
|
||||||
|
}];
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.unlinkOrcidByItem(mockItemLinkedToOrcid).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(researcherProfile, operations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrcidAuthorizeUrl', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
routerStub.setRoute('/entities/person/uuid/orcid');
|
||||||
|
(service as any).configurationService.findByPropertyName.and.returnValues(
|
||||||
|
createSuccessfulRemoteDataObject$(authorizeUrl),
|
||||||
|
createSuccessfulRemoteDataObject$(appClientId),
|
||||||
|
createSuccessfulRemoteDataObject$(orcidScope)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build the url properly', () => {
|
||||||
|
const result = service.getOrcidAuthorizeUrl(mockItemUnlinkedToOrcid);
|
||||||
|
const redirectUri: string = new URLCombiner(nativeWindowService.nativeWindow.origin, encodeURIComponent(routerStub.url.split('?')[0])).toString();
|
||||||
|
const url = 'orcid.authorize-url?client_id=orcid.application-client-id&redirect_uri=' + redirectUri + '&response_type=code&scope=/authenticate /read-limited';
|
||||||
|
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: url
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrcidAuthorizationScopesByItem', () => {
|
||||||
|
it('should return list of scopes saved in the item', () => {
|
||||||
|
const orcidScopes = [
|
||||||
|
'/authenticate',
|
||||||
|
'/read-limited',
|
||||||
|
'/activities/update',
|
||||||
|
'/person/update'
|
||||||
|
];
|
||||||
|
const result = service.getOrcidAuthorizationScopesByItem(mockItemLinkedToOrcid);
|
||||||
|
expect(result).toEqual(orcidScopes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOrcidAuthorizationScopes', () => {
|
||||||
|
it('should return list of scopes by configuration', () => {
|
||||||
|
(service as any).configurationService.findByPropertyName.and.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$(orcidScope)
|
||||||
|
);
|
||||||
|
const orcidScopes = [
|
||||||
|
'/authenticate',
|
||||||
|
'/read-limited'
|
||||||
|
];
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: orcidScopes
|
||||||
|
});
|
||||||
|
const result = service.getOrcidAuthorizationScopes();
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
145
src/app/core/orcid/orcid-auth.service.ts
Normal file
145
src/app/core/orcid/orcid-auth.service.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { AddOperation, RemoveOperation } from 'fast-json-patch';
|
||||||
|
|
||||||
|
import { ResearcherProfileService } from '../profile/researcher-profile.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { ResearcherProfile } from '../profile/model/researcher-profile.model';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrcidAuthService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
private configurationService: ConfigurationDataService,
|
||||||
|
private researcherProfileService: ResearcherProfileService,
|
||||||
|
private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given item is linked to an ORCID profile.
|
||||||
|
*
|
||||||
|
* @param item the item to check
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
public isLinkedToOrcid(item: Item): boolean {
|
||||||
|
return item.hasMetadata('dspace.orcid.authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if only the admin users can disconnect a researcher profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
public onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
||||||
|
return propertyRD.hasSucceeded && propertyRD.payload.values.map((value) => value.toLowerCase()).includes('only_admin');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the profile's owner can disconnect that profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
public ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
||||||
|
return propertyRD.hasSucceeded && propertyRD.payload.values.map( (value) => value.toLowerCase()).includes('admin_and_owner');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a link operation to ORCID profile.
|
||||||
|
*
|
||||||
|
* @param person The person item related to the researcher profile
|
||||||
|
* @param code The auth-code received from orcid
|
||||||
|
*/
|
||||||
|
public linkOrcidByItem(person: Item, code: string): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
const operations: AddOperation<string>[] = [{
|
||||||
|
path: '/orcid',
|
||||||
|
op: 'add',
|
||||||
|
value: code
|
||||||
|
}];
|
||||||
|
|
||||||
|
return this.researcherProfileService.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((profileRD) => this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform unlink operation from ORCID profile.
|
||||||
|
*
|
||||||
|
* @param person The person item related to the researcher profile
|
||||||
|
*/
|
||||||
|
public unlinkOrcidByItem(person: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
const operations: RemoveOperation[] = [{
|
||||||
|
path:'/orcid',
|
||||||
|
op:'remove'
|
||||||
|
}];
|
||||||
|
|
||||||
|
return this.researcherProfileService.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((profileRD) => this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and return the url to authenticate with orcid
|
||||||
|
*
|
||||||
|
* @param profile
|
||||||
|
*/
|
||||||
|
public getOrcidAuthorizeUrl(profile: Item): Observable<string> {
|
||||||
|
return combineLatest([
|
||||||
|
this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.configurationService.findByPropertyName('orcid.scope').pipe(getFirstSucceededRemoteDataPayload())]
|
||||||
|
).pipe(
|
||||||
|
map(([authorizeUrl, clientId, scopes]) => {
|
||||||
|
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
||||||
|
console.log(redirectUri.toString());
|
||||||
|
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
||||||
|
+ scopes.values.join(' ');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all orcid authorization scopes saved in the given item
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
public getOrcidAuthorizationScopesByItem(item: Item): string[] {
|
||||||
|
return isNotEmpty(item) ? item.allMetadataValues('dspace.orcid.scope') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all orcid authorization scopes available by configuration
|
||||||
|
*/
|
||||||
|
public getOrcidAuthorizationScopes(): Observable<string[]> {
|
||||||
|
return this.configurationService.findByPropertyName('orcid.scope').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrcidDisconnectionAllowedUsersConfiguration(): Observable<RemoteData<ConfigurationProperty>> {
|
||||||
|
return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
126
src/app/core/orcid/orcid-history-data.service.ts
Normal file
126
src/app/core/orcid/orcid-history-data.service.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { PostRequest } from '../data/request.models';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { OrcidHistory } from './model/orcid-history.model';
|
||||||
|
import { ORCID_HISTORY } from './model/orcid-history.resource-type';
|
||||||
|
import { OrcidQueue } from './model/orcid-queue.model';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { CoreState } from '../core-state.model';
|
||||||
|
import { RestRequest } from '../data/rest-request.model';
|
||||||
|
import { sendRequest } from '../shared/request.operators';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private DataService implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
class OrcidHistoryServiceImpl extends DataService<OrcidHistory> {
|
||||||
|
public linkPath = 'orcidhistories';
|
||||||
|
|
||||||
|
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<OrcidHistory>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to make REST requests with Orcid History endpoint.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(ORCID_HISTORY)
|
||||||
|
export class OrcidHistoryDataService {
|
||||||
|
|
||||||
|
dataService: OrcidHistoryServiceImpl;
|
||||||
|
|
||||||
|
responseMsToLive: number = 10 * 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<OrcidHistory>,
|
||||||
|
protected itemService: ItemDataService ) {
|
||||||
|
|
||||||
|
this.dataService = new OrcidHistoryServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||||
|
notificationsService, http, comparator);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToORCID(orcidQueue: OrcidQueue): Observable<RemoteData<OrcidHistory>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
return this.getEndpoint().pipe(
|
||||||
|
map((endpointURL: string) => {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
return new PostRequest(requestId, endpointURL, orcidQueue._links.self.href, options);
|
||||||
|
}),
|
||||||
|
sendRequest(this.requestService),
|
||||||
|
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable<RemoteData<OrcidHistory>>)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndpoint(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.dataService.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
|
||||||
|
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||||
|
* @param id ID of object we want to retrieve
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<OrcidHistory>[]): Observable<RemoteData<OrcidHistory>> {
|
||||||
|
return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of observables of {@link RemoteData} of {@link OrcidHistory}s, based on an href, with a list of {@link FollowLinkConfig},
|
||||||
|
* to automatically resolve {@link HALLink}s of the {@link OrcidHistory}
|
||||||
|
* @param href The url of object we want to retrieve
|
||||||
|
* @param findListOptions Find list options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
|
* {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<OrcidHistory>[]): Observable<RemoteData<PaginatedList<OrcidHistory>>> {
|
||||||
|
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
110
src/app/core/orcid/orcid-queue.service.ts
Normal file
110
src/app/core/orcid/orcid-queue.service.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { OrcidQueue } from './model/orcid-queue.model';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
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 '../data/default-change-analyzer.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { ORCID_QUEUE } from './model/orcid-queue.resource-type';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CoreState } from '../core-state.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private DataService implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
class OrcidQueueServiceImpl extends DataService<OrcidQueue> {
|
||||||
|
public linkPath = 'orcidqueues';
|
||||||
|
|
||||||
|
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<OrcidQueue>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to make REST requests with Orcid Queue endpoint.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(ORCID_QUEUE)
|
||||||
|
export class OrcidQueueService {
|
||||||
|
|
||||||
|
dataService: OrcidQueueServiceImpl;
|
||||||
|
|
||||||
|
responseMsToLive: number = 10 * 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<OrcidQueue>,
|
||||||
|
protected configurationService: ConfigurationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected itemService: ItemDataService ) {
|
||||||
|
|
||||||
|
this.dataService = new OrcidQueueServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||||
|
notificationsService, http, comparator);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param itemId It represent an Id of profileItem
|
||||||
|
* @param paginationOptions The pagination options object
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @returns { OrcidQueue }
|
||||||
|
*/
|
||||||
|
searchByProfileItemId(itemId: string, paginationOptions: PaginationComponentOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
|
||||||
|
return this.dataService.searchBy('findByProfileItem', {
|
||||||
|
searchParams: [new RequestParam('profileItemId', itemId)],
|
||||||
|
elementsPerPage: paginationOptions.pageSize,
|
||||||
|
currentPage: paginationOptions.currentPage
|
||||||
|
},
|
||||||
|
useCachedVersionIfAvailable,
|
||||||
|
reRequestOnStale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param orcidQueueId represents a id of orcid queue
|
||||||
|
* @returns { NoContent }
|
||||||
|
*/
|
||||||
|
deleteById(orcidQueueId: number): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.dataService.delete(orcidQueueId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will set linkPath to stale
|
||||||
|
*/
|
||||||
|
clearFindByProfileItemRequests() {
|
||||||
|
this.requestService.setStaleByHrefSubstring(this.dataService.linkPath + '/search/findByProfileItem');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -12,7 +12,6 @@ import { RequestService } from '../data/request.service';
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { buildPaginatedList } from '../data/paginated-list.model';
|
import { buildPaginatedList } from '../data/paginated-list.model';
|
||||||
import {
|
import {
|
||||||
createFailedRemoteDataObject$,
|
|
||||||
createNoContentRemoteDataObject$,
|
createNoContentRemoteDataObject$,
|
||||||
createSuccessfulRemoteDataObject,
|
createSuccessfulRemoteDataObject,
|
||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
@@ -23,15 +22,12 @@ import { ResearcherProfileService } from './researcher-profile.service';
|
|||||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
import { ResearcherProfile } from './model/researcher-profile.model';
|
import { ResearcherProfile } from './model/researcher-profile.model';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { AddOperation, RemoveOperation, ReplaceOperation } from 'fast-json-patch';
|
import { ReplaceOperation } from 'fast-json-patch';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { PostRequest } from '../data/request.models';
|
import { PostRequest } from '../data/request.models';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
import { ConfigurationDataService } from '../data/configuration-data.service';
|
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { NativeWindowRefMock } from '../../shared/mocks/mock-native-window-ref';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
|
||||||
|
|
||||||
describe('ResearcherProfileService', () => {
|
describe('ResearcherProfileService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -42,8 +38,6 @@ describe('ResearcherProfileService', () => {
|
|||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let responseCacheEntry: RequestEntry;
|
let responseCacheEntry: RequestEntry;
|
||||||
let configurationDataService: ConfigurationDataService;
|
|
||||||
let nativeWindowService: NativeWindowRefMock;
|
|
||||||
let routerStub: any;
|
let routerStub: any;
|
||||||
|
|
||||||
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||||
@@ -252,13 +246,8 @@ describe('ResearcherProfileService', () => {
|
|||||||
const itemService = jasmine.createSpyObj('ItemService', {
|
const itemService = jasmine.createSpyObj('ItemService', {
|
||||||
findByHref: jasmine.createSpy('findByHref')
|
findByHref: jasmine.createSpy('findByHref')
|
||||||
});
|
});
|
||||||
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
|
||||||
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
|
||||||
});
|
|
||||||
nativeWindowService = new NativeWindowRefMock();
|
|
||||||
|
|
||||||
service = new ResearcherProfileService(
|
service = new ResearcherProfileService(
|
||||||
nativeWindowService,
|
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
objectCache,
|
objectCache,
|
||||||
@@ -267,8 +256,7 @@ describe('ResearcherProfileService', () => {
|
|||||||
http,
|
http,
|
||||||
routerStub,
|
routerStub,
|
||||||
comparator,
|
comparator,
|
||||||
itemService,
|
itemService
|
||||||
configurationDataService
|
|
||||||
);
|
);
|
||||||
serviceAsAny = service;
|
serviceAsAny = service;
|
||||||
|
|
||||||
@@ -415,121 +403,6 @@ describe('ResearcherProfileService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isLinkedToOrcid', () => {
|
|
||||||
it('should return true when item has metadata', () => {
|
|
||||||
const result = service.isLinkedToOrcid(mockItemLinkedToOrcid);
|
|
||||||
expect(result).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true when item has no metadata', () => {
|
|
||||||
const result = service.isLinkedToOrcid(mockItemUnlinkedToOrcid);
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('onlyAdminCanDisconnectProfileFromOrcid', () => {
|
|
||||||
it('should return true when property is only_admin', () => {
|
|
||||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdmin));
|
|
||||||
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
|
||||||
const expected = cold('(a|)', {
|
|
||||||
a: true
|
|
||||||
});
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false on faild', () => {
|
|
||||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
|
||||||
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
|
||||||
const expected = cold('(a|)', {
|
|
||||||
a: false
|
|
||||||
});
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ownerCanDisconnectProfileFromOrcid', () => {
|
|
||||||
it('should return true when property is admin_and_owner', () => {
|
|
||||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdminOwner));
|
|
||||||
const result = service.ownerCanDisconnectProfileFromOrcid();
|
|
||||||
const expected = cold('(a|)', {
|
|
||||||
a: true
|
|
||||||
});
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false on faild', () => {
|
|
||||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
|
||||||
const result = service.ownerCanDisconnectProfileFromOrcid();
|
|
||||||
const expected = cold('(a|)', {
|
|
||||||
a: false
|
|
||||||
});
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('linkOrcidByItem', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
|
||||||
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call patch method properly', () => {
|
|
||||||
const operations: AddOperation<string>[] = [{
|
|
||||||
path: '/orcid',
|
|
||||||
op: 'add',
|
|
||||||
value: 'test-code'
|
|
||||||
}];
|
|
||||||
|
|
||||||
scheduler.schedule(() => service.linkOrcidByItem(mockItemUnlinkedToOrcid, 'test-code').subscribe());
|
|
||||||
scheduler.flush();
|
|
||||||
|
|
||||||
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, operations);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('unlinkOrcidByItem', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
|
||||||
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call patch method properly', () => {
|
|
||||||
const operations: RemoveOperation[] = [{
|
|
||||||
path: '/orcid',
|
|
||||||
op: 'remove'
|
|
||||||
}];
|
|
||||||
|
|
||||||
scheduler.schedule(() => service.unlinkOrcidByItem(mockItemLinkedToOrcid).subscribe());
|
|
||||||
scheduler.flush();
|
|
||||||
|
|
||||||
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, operations);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getOrcidAuthorizeUrl', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
routerStub.setRoute('/entities/person/uuid/orcid');
|
|
||||||
(service as any).configurationService.findByPropertyName.and.returnValues(
|
|
||||||
createSuccessfulRemoteDataObject$(authorizeUrl),
|
|
||||||
createSuccessfulRemoteDataObject$(appClientId),
|
|
||||||
createSuccessfulRemoteDataObject$(orcidScope)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should build the url properly', () => {
|
|
||||||
const result = service.getOrcidAuthorizeUrl(mockItemUnlinkedToOrcid);
|
|
||||||
const redirectUri: string = new URLCombiner(nativeWindowService.nativeWindow.origin, encodeURIComponent(routerStub.url.split('?')[0])).toString();
|
|
||||||
const url = 'orcid.authorize-url?client_id=orcid.application-client-id&redirect_uri=' + redirectUri + '&response_type=code&scope=/authenticate /read-limited';
|
|
||||||
|
|
||||||
const expected = cold('(a|)', {
|
|
||||||
a: url
|
|
||||||
});
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateByOrcidOperations', () => {
|
describe('updateByOrcidOperations', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
@@ -543,34 +416,4 @@ describe('ResearcherProfileService', () => {
|
|||||||
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []);
|
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getOrcidAuthorizationScopesByItem', () => {
|
|
||||||
it('should return list of scopes saved in the item', () => {
|
|
||||||
const orcidScopes = [
|
|
||||||
'/authenticate',
|
|
||||||
'/read-limited',
|
|
||||||
'/activities/update',
|
|
||||||
'/person/update'
|
|
||||||
];
|
|
||||||
const result = service.getOrcidAuthorizationScopesByItem(mockItemLinkedToOrcid);
|
|
||||||
expect(result).toEqual(orcidScopes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getOrcidAuthorizationScopes', () => {
|
|
||||||
it('should return list of scopes by configuration', () => {
|
|
||||||
(service as any).configurationService.findByPropertyName.and.returnValue(
|
|
||||||
createSuccessfulRemoteDataObject$(orcidScope)
|
|
||||||
);
|
|
||||||
const orcidScopes = [
|
|
||||||
'/authenticate',
|
|
||||||
'/read-limited'
|
|
||||||
];
|
|
||||||
const expected = cold('(a|)', {
|
|
||||||
a: orcidScopes
|
|
||||||
});
|
|
||||||
const result = service.getOrcidAuthorizationScopes();
|
|
||||||
expect(result).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,41 +1,33 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AddOperation, Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch';
|
import { Operation, ReplaceOperation } from 'fast-json-patch';
|
||||||
import { combineLatest, Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { find, map, switchMap } from 'rxjs/operators';
|
import { find, map } from 'rxjs/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ConfigurationDataService } from '../data/configuration-data.service';
|
|
||||||
import { DataService } from '../data/data.service';
|
import { DataService } from '../data/data.service';
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
import { ItemDataService } from '../data/item-data.service';
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
import {
|
import { getAllCompletedRemoteData, getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
getAllCompletedRemoteData,
|
|
||||||
getFirstCompletedRemoteData,
|
|
||||||
getFirstSucceededRemoteDataPayload
|
|
||||||
} from '../shared/operators';
|
|
||||||
import { ResearcherProfile } from './model/researcher-profile.model';
|
import { ResearcherProfile } from './model/researcher-profile.model';
|
||||||
import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type';
|
import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { PostRequest } from '../data/request.models';
|
import { PostRequest } from '../data/request.models';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty } from '../../shared/empty.util';
|
||||||
import { CoreState } from '../core-state.model';
|
import { CoreState } from '../core-state.model';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A private DataService implementation to delegate specific methods to.
|
* A private DataService implementation to delegate specific methods to.
|
||||||
@@ -69,7 +61,6 @@ export class ResearcherProfileService {
|
|||||||
protected responseMsToLive: number = 10 * 1000;
|
protected responseMsToLive: number = 10 * 1000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
@@ -78,8 +69,7 @@ export class ResearcherProfileService {
|
|||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>,
|
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>,
|
||||||
protected itemService: ItemDataService,
|
protected itemService: ItemDataService) {
|
||||||
protected configurationService: ConfigurationDataService) {
|
|
||||||
|
|
||||||
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService,
|
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService,
|
||||||
notificationsService, http, comparator);
|
notificationsService, http, comparator);
|
||||||
@@ -165,98 +155,6 @@ export class ResearcherProfileService {
|
|||||||
return this.dataService.patch(researcherProfile, [replaceOperation]);
|
return this.dataService.patch(researcherProfile, [replaceOperation]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given item is linked to an ORCID profile.
|
|
||||||
*
|
|
||||||
* @param item the item to check
|
|
||||||
* @returns the check result
|
|
||||||
*/
|
|
||||||
public isLinkedToOrcid(item: Item): boolean {
|
|
||||||
return item.hasMetadata('dspace.orcid.authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if only the admin users can disconnect a researcher profile from ORCID.
|
|
||||||
*
|
|
||||||
* @returns the check result
|
|
||||||
*/
|
|
||||||
public onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
|
||||||
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
|
||||||
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
|
||||||
return propertyRD.hasSucceeded && propertyRD.payload.values.map((value) => value.toLowerCase()).includes('only_admin');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the profile's owner can disconnect that profile from ORCID.
|
|
||||||
*
|
|
||||||
* @returns the check result
|
|
||||||
*/
|
|
||||||
public ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
|
||||||
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
|
||||||
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
|
||||||
return propertyRD.hasSucceeded && propertyRD.payload.values.map( (value) => value.toLowerCase()).includes('admin_and_owner');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a link operation to ORCID profile.
|
|
||||||
*
|
|
||||||
* @param person The person item related to the researcher profile
|
|
||||||
* @param code The auth-code received from orcid
|
|
||||||
*/
|
|
||||||
public linkOrcidByItem(person: Item, code: string): Observable<RemoteData<ResearcherProfile>> {
|
|
||||||
const operations: AddOperation<string>[] = [{
|
|
||||||
path: '/orcid',
|
|
||||||
op: 'add',
|
|
||||||
value: code
|
|
||||||
}];
|
|
||||||
|
|
||||||
return this.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
switchMap((profileRD) => this.updateByOrcidOperations(profileRD.payload, operations))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform unlink operation from ORCID profile.
|
|
||||||
*
|
|
||||||
* @param person The person item related to the researcher profile
|
|
||||||
*/
|
|
||||||
public unlinkOrcidByItem(person: Item): Observable<RemoteData<ResearcherProfile>> {
|
|
||||||
const operations: RemoveOperation[] = [{
|
|
||||||
path:'/orcid',
|
|
||||||
op:'remove'
|
|
||||||
}];
|
|
||||||
|
|
||||||
return this.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
switchMap((profileRD) => this.updateByOrcidOperations(profileRD.payload, operations))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build and return the url to authenticate with orcid
|
|
||||||
*
|
|
||||||
* @param profile
|
|
||||||
*/
|
|
||||||
public getOrcidAuthorizeUrl(profile: Item): Observable<string> {
|
|
||||||
return combineLatest([
|
|
||||||
this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()),
|
|
||||||
this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()),
|
|
||||||
this.configurationService.findByPropertyName('orcid.scope').pipe(getFirstSucceededRemoteDataPayload())]
|
|
||||||
).pipe(
|
|
||||||
map(([authorizeUrl, clientId, scopes]) => {
|
|
||||||
console.log(this._window.nativeWindow.origin, this.router.url);
|
|
||||||
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
|
||||||
console.log(redirectUri.toString());
|
|
||||||
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
|
||||||
+ scopes.values.join(' ');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a researcher profile starting from an external source URI
|
* Creates a researcher profile starting from an external source URI
|
||||||
* @param sourceUri URI of source item of researcher profile.
|
* @param sourceUri URI of source item of researcher profile.
|
||||||
@@ -291,29 +189,6 @@ export class ResearcherProfileService {
|
|||||||
return this.dataService.patch(researcherProfile, operations);
|
return this.dataService.patch(researcherProfile, operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all orcid authorization scopes saved in the given item
|
|
||||||
*
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
public getOrcidAuthorizationScopesByItem(item: Item): string[] {
|
|
||||||
return isNotEmpty(item) ? item.allMetadataValues('dspace.orcid.scope') : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all orcid authorization scopes available by configuration
|
|
||||||
*/
|
|
||||||
public getOrcidAuthorizationScopes(): Observable<string[]> {
|
|
||||||
return this.configurationService.findByPropertyName('orcid.scope').pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOrcidDisconnectionAllowedUsersConfiguration(): Observable<RemoteData<ConfigurationProperty>> {
|
|
||||||
return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe(
|
|
||||||
getFirstCompletedRemoteData()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -48,6 +48,7 @@ import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component
|
|||||||
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
|
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
|
||||||
|
import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component';
|
||||||
|
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -83,7 +84,8 @@ const DECLARATIONS = [
|
|||||||
VersionPageComponent,
|
VersionPageComponent,
|
||||||
OrcidPageComponent,
|
OrcidPageComponent,
|
||||||
OrcidAuthComponent,
|
OrcidAuthComponent,
|
||||||
OrcidSyncSettingsComponent
|
OrcidSyncSettingsComponent,
|
||||||
|
OrcidQueueComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -9,7 +9,7 @@ import { of } from 'rxjs';
|
|||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
@@ -25,7 +25,7 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
let comp: OrcidAuthComponent;
|
let comp: OrcidAuthComponent;
|
||||||
let fixture: ComponentFixture<OrcidAuthComponent>;
|
let fixture: ComponentFixture<OrcidAuthComponent>;
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||||
let nativeWindowRef;
|
let nativeWindowRef;
|
||||||
let notificationsService;
|
let notificationsService;
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
orcidAuthService = jasmine.createSpyObj('researcherProfileService', {
|
||||||
getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'),
|
getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'),
|
||||||
getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'),
|
getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'),
|
||||||
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'),
|
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'),
|
||||||
@@ -137,7 +137,7 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
{ provide: ResearcherProfileService, useValue: researcherProfileService }
|
{ provide: OrcidAuthService, useValue: orcidAuthService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(OrcidAuthComponent, {
|
}).overrideComponent(OrcidAuthComponent, {
|
||||||
@@ -149,17 +149,17 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
fixture = TestBed.createComponent(OrcidAuthComponent);
|
fixture = TestBed.createComponent(OrcidAuthComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
researcherProfileService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
|
orcidAuthService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('when orcid profile is not linked', () => {
|
describe('when orcid profile is not linked', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemUnlinkedToOrcid;
|
comp.item = mockItemUnlinkedToOrcid;
|
||||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(false);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(false);
|
||||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
researcherProfileService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl'));
|
orcidAuthService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl'));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
describe('when orcid profile is linked', () => {
|
describe('when orcid profile is linked', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
@@ -191,16 +191,16 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
notificationsService = (comp as any).notificationsService;
|
notificationsService = (comp as any).notificationsService;
|
||||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('and unlink is successfully', () => {
|
describe('and unlink is successfully', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile()));
|
orcidAuthService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile()));
|
||||||
spyOn(comp.unlink, 'emit');
|
spyOn(comp.unlink, 'emit');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
@@ -217,7 +217,7 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
describe('and unlink is failed', () => {
|
describe('and unlink is failed', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
orcidAuthService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -234,10 +234,10 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -263,10 +263,10 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -294,10 +294,10 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -314,10 +314,10 @@ describe('OrcidAuthComponent test suite', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
comp.item = mockItemLinkedToOrcid;
|
comp.item = mockItemLinkedToOrcid;
|
||||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -3,14 +3,13 @@ import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, Simp
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
|
||||||
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-orcid-auth',
|
selector: 'ds-orcid-auth',
|
||||||
@@ -65,7 +64,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
|
|||||||
@Output() unlink: EventEmitter<void> = new EventEmitter<void>();
|
@Output() unlink: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private researcherProfileService: ResearcherProfileService,
|
private orcidAuthService: OrcidAuthService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
@@ -73,7 +72,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.researcherProfileService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
|
this.orcidAuthService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
|
||||||
this.orcidAuthorizationScopes.next(scopes);
|
this.orcidAuthorizationScopes.next(scopes);
|
||||||
this.initOrcidAuthSettings();
|
this.initOrcidAuthSettings();
|
||||||
});
|
});
|
||||||
@@ -160,7 +159,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
|
|||||||
* Link existing person profile with orcid
|
* Link existing person profile with orcid
|
||||||
*/
|
*/
|
||||||
linkOrcid(): void {
|
linkOrcid(): void {
|
||||||
this.researcherProfileService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
|
this.orcidAuthService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
|
||||||
this._window.nativeWindow.location.href = authorizeUrl;
|
this._window.nativeWindow.location.href = authorizeUrl;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -170,7 +169,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
unlinkOrcid(): void {
|
unlinkOrcid(): void {
|
||||||
this.unlinkProcessing.next(true);
|
this.unlinkProcessing.next(true);
|
||||||
this.researcherProfileService.unlinkOrcidByItem(this.item).pipe(
|
this.orcidAuthService.unlinkOrcidByItem(this.item).pipe(
|
||||||
getFirstCompletedRemoteData()
|
getFirstCompletedRemoteData()
|
||||||
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
||||||
this.unlinkProcessing.next(false);
|
this.unlinkProcessing.next(false);
|
||||||
@@ -193,19 +192,19 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
this.setMissingOrcidAuthorizations();
|
this.setMissingOrcidAuthorizations();
|
||||||
|
|
||||||
this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
|
this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||||
this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
|
this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.researcherProfileService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
|
this.orcidAuthService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||||
this.ownerCanDisconnectProfileFromOrcid$.next(result);
|
this.ownerCanDisconnectProfileFromOrcid$.next(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isOrcidLinked$.next(this.researcherProfileService.isLinkedToOrcid(this.item));
|
this.isOrcidLinked$.next(this.orcidAuthService.isLinkedToOrcid(this.item));
|
||||||
}
|
}
|
||||||
|
|
||||||
private setMissingOrcidAuthorizations(): void {
|
private setMissingOrcidAuthorizations(): void {
|
||||||
const profileScopes = this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item);
|
const profileScopes = this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item);
|
||||||
const orcidScopes = this.orcidAuthorizationScopes.value;
|
const orcidScopes = this.orcidAuthorizationScopes.value;
|
||||||
const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope));
|
const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope));
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setOrcidAuthorizationsFromItem(): void {
|
private setOrcidAuthorizationsFromItem(): void {
|
||||||
this.profileAuthorizationScopes.next(this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item));
|
this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,4 +15,5 @@
|
|||||||
<ng-container *ngIf="!(processingConnection | async) && (item | async) && (connectionStatus | async)" >
|
<ng-container *ngIf="!(processingConnection | async) && (item | async) && (connectionStatus | async)" >
|
||||||
<ds-orcid-auth [item]="(item | async)" (unlink)="updateItem()" data-test="orcid-auth"></ds-orcid-auth>
|
<ds-orcid-auth [item]="(item | async)" (unlink)="updateItem()" data-test="orcid-auth"></ds-orcid-auth>
|
||||||
<ds-orcid-sync-setting *ngIf="isLinkedToOrcid()" [item]="(item | async)" (settingsUpdated)="updateItem()" data-test="orcid-sync-setting"></ds-orcid-sync-setting>
|
<ds-orcid-sync-setting *ngIf="isLinkedToOrcid()" [item]="(item | async)" (settingsUpdated)="updateItem()" data-test="orcid-sync-setting"></ds-orcid-sync-setting>
|
||||||
|
<ds-orcid-queue *ngIf="isLinkedToOrcid()" [item]="(item | async)"></ds-orcid-queue>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -11,7 +11,6 @@ import { getTestScheduler } from 'jasmine-marbles';
|
|||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
|
|
||||||
import { OrcidPageComponent } from './orcid-page.component';
|
import { OrcidPageComponent } from './orcid-page.component';
|
||||||
import {
|
import {
|
||||||
createFailedRemoteDataObject$,
|
createFailedRemoteDataObject$,
|
||||||
@@ -23,6 +22,7 @@ import { createPaginatedList } from '../../shared/testing/utils.test';
|
|||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||||
|
import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
describe('OrcidPageComponent test suite', () => {
|
describe('OrcidPageComponent test suite', () => {
|
||||||
let comp: OrcidPageComponent;
|
let comp: OrcidPageComponent;
|
||||||
@@ -32,7 +32,7 @@ describe('OrcidPageComponent test suite', () => {
|
|||||||
let routeStub: jasmine.SpyObj<ActivatedRouteStub>;
|
let routeStub: jasmine.SpyObj<ActivatedRouteStub>;
|
||||||
let routeData: any;
|
let routeData: any;
|
||||||
let itemDataService: jasmine.SpyObj<ItemDataService>;
|
let itemDataService: jasmine.SpyObj<ItemDataService>;
|
||||||
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||||
|
|
||||||
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||||
id: 'test-id',
|
id: 'test-id',
|
||||||
@@ -88,7 +88,7 @@ describe('OrcidPageComponent test suite', () => {
|
|||||||
|
|
||||||
routeStub = new ActivatedRouteStub({}, routeData);
|
routeStub = new ActivatedRouteStub({}, routeData);
|
||||||
|
|
||||||
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
|
||||||
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||||
linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
|
linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
|
||||||
});
|
});
|
||||||
@@ -110,7 +110,7 @@ describe('OrcidPageComponent test suite', () => {
|
|||||||
declarations: [OrcidPageComponent],
|
declarations: [OrcidPageComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: ResearcherProfileService, useValue: researcherProfileService },
|
{ provide: OrcidAuthService, useValue: orcidAuthService },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: ItemDataService, useValue: itemDataService },
|
{ provide: ItemDataService, useValue: itemDataService },
|
||||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
@@ -146,7 +146,7 @@ describe('OrcidPageComponent test suite', () => {
|
|||||||
it('should call isLinkedToOrcid', () => {
|
it('should call isLinkedToOrcid', () => {
|
||||||
comp.isLinkedToOrcid();
|
comp.isLinkedToOrcid();
|
||||||
|
|
||||||
expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value);
|
expect(orcidAuthService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update item', fakeAsync(() => {
|
it('should update item', fakeAsync(() => {
|
||||||
@@ -168,13 +168,13 @@ describe('OrcidPageComponent test suite', () => {
|
|||||||
|
|
||||||
describe('and linking to orcid profile is successfully', () => {
|
describe('and linking to orcid profile is successfully', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
researcherProfileService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
orcidAuthService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should call linkOrcidByItem', () => {
|
it('should call linkOrcidByItem', () => {
|
||||||
expect(researcherProfileService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||||
expect(comp.updateItem).toHaveBeenCalled();
|
expect(comp.updateItem).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,13 +193,13 @@ describe('OrcidPageComponent test suite', () => {
|
|||||||
|
|
||||||
describe('and linking to orcid profile is failed', () => {
|
describe('and linking to orcid profile is failed', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
researcherProfileService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
orcidAuthService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should call linkOrcidByItem', () => {
|
it('should call linkOrcidByItem', () => {
|
||||||
expect(researcherProfileService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||||
expect(comp.updateItem).not.toHaveBeenCalled();
|
expect(comp.updateItem).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest } from 'rxjs';
|
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
|
import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
|
||||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
@@ -14,7 +15,6 @@ import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
|||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component that represents the orcid settings page
|
* A component that represents the orcid settings page
|
||||||
@@ -50,7 +50,7 @@ export class OrcidPageComponent implements OnInit {
|
|||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private itemService: ItemDataService,
|
private itemService: ItemDataService,
|
||||||
private researcherProfileService: ResearcherProfileService,
|
private orcidAuthService: OrcidAuthService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {
|
) {
|
||||||
@@ -95,7 +95,7 @@ export class OrcidPageComponent implements OnInit {
|
|||||||
* @returns the check result
|
* @returns the check result
|
||||||
*/
|
*/
|
||||||
isLinkedToOrcid(): boolean {
|
isLinkedToOrcid(): boolean {
|
||||||
return this.researcherProfileService.isLinkedToOrcid(this.item.value);
|
return this.orcidAuthService.isLinkedToOrcid(this.item.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +109,7 @@ export class OrcidPageComponent implements OnInit {
|
|||||||
* Retrieve the updated profile item
|
* Retrieve the updated profile item
|
||||||
*/
|
*/
|
||||||
updateItem(): void {
|
updateItem(): void {
|
||||||
|
this.clearRouteParams();
|
||||||
this.itemService.findById(this.itemId, false).pipe(
|
this.itemService.findById(this.itemId, false).pipe(
|
||||||
getFirstCompletedRemoteData()
|
getFirstCompletedRemoteData()
|
||||||
).subscribe((itemRD: RemoteData<Item>) => {
|
).subscribe((itemRD: RemoteData<Item>) => {
|
||||||
@@ -125,7 +126,7 @@ export class OrcidPageComponent implements OnInit {
|
|||||||
* @param code The auth-code received from ORCID
|
* @param code The auth-code received from ORCID
|
||||||
*/
|
*/
|
||||||
private linkProfileToOrcid(person: Item, code: string) {
|
private linkProfileToOrcid(person: Item, code: string) {
|
||||||
this.researcherProfileService.linkOrcidByItem(person, code).pipe(
|
this.orcidAuthService.linkOrcidByItem(person, code).pipe(
|
||||||
getFirstCompletedRemoteData()
|
getFirstCompletedRemoteData()
|
||||||
).subscribe((profileRD: RemoteData<ResearcherProfile>) => {
|
).subscribe((profileRD: RemoteData<ResearcherProfile>) => {
|
||||||
this.processingConnection.next(false);
|
this.processingConnection.next(false);
|
||||||
@@ -135,11 +136,18 @@ export class OrcidPageComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.item.next(person);
|
this.item.next(person);
|
||||||
this.connectionStatus.next(false);
|
this.connectionStatus.next(false);
|
||||||
|
this.clearRouteParams();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update route removing the code from query params
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private clearRouteParams(): void {
|
||||||
// update route removing the code from query params
|
// update route removing the code from query params
|
||||||
const redirectUrl = this.router.url.split('?')[0];
|
const redirectUrl = this.router.url.split('?')[0];
|
||||||
this.router.navigate([redirectUrl]);
|
this.router.navigate([redirectUrl]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
<div>
|
||||||
|
<ds-loading *ngIf="(processing$ | async)"></ds-loading>
|
||||||
|
<div class="container">
|
||||||
|
<h2>{{ 'person.orcid.registry.queue' | translate }}</h2>
|
||||||
|
|
||||||
|
<ds-alert *ngIf="!(processing$ | async) && (getList() | async)?.payload?.totalElements == 0"
|
||||||
|
[type]="AlertTypeEnum.Info">
|
||||||
|
{{ 'person.page.orcid.sync-queue.empty-message' | translate}}
|
||||||
|
</ds-alert>
|
||||||
|
<ds-pagination *ngIf="!(processing$ | async) && (getList() | async)?.payload?.totalElements > 0"
|
||||||
|
[paginationOptions]="paginationOptions"
|
||||||
|
[collectionSize]="(getList() | async)?.payload?.totalElements"
|
||||||
|
[retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()">
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="groups" class="table table-sm table-striped table-hover table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-center align-middle">
|
||||||
|
<th>{{'person.page.orcid.sync-queue.table.header.type' | translate}}</th>
|
||||||
|
<th>{{'person.page.orcid.sync-queue.table.header.description' | translate}}</th>
|
||||||
|
<th>{{'person.page.orcid.sync-queue.table.header.action' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let entry of (getList() | async)?.payload?.page" data-test="orcidQueueElementRow">
|
||||||
|
<td style="width: 15%" class="text-center align-middle">
|
||||||
|
<i [ngClass]="getIconClass(entry)" [ngbTooltip]="getIconTooltip(entry) | translate"
|
||||||
|
class="fa-2x" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
|
<td class="text-center align-middle">
|
||||||
|
{{ entry.description }}
|
||||||
|
</td>
|
||||||
|
<td style="width: 20%" class="text-center">
|
||||||
|
<div class="btn-group edit-field">
|
||||||
|
<button [ngbTooltip]="getOperationTooltip(entry) | translate" container="body"
|
||||||
|
class="btn btn-outline-primary my-1 col-md" (click)="send(entry)">
|
||||||
|
<i [ngClass]="getOperationClass(entry)"></i>
|
||||||
|
</button>
|
||||||
|
<button [ngbTooltip]="'person.page.orcid.sync-queue.discard' | translate" container="body"
|
||||||
|
class="btn btn-outline-danger my-1 col-md" (click)="discardEntry(entry)">
|
||||||
|
<i class="fas fa-unlink"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,151 @@
|
|||||||
|
import { OrcidQueueComponent } from './orcid-queue.component';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { OrcidQueueService } from '../../../core/orcid/orcid-queue.service';
|
||||||
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { OrcidHistoryDataService } from '../../../core/orcid/orcid-history-data.service';
|
||||||
|
import { OrcidQueue } from '../../../core/orcid/model/orcid-queue.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
|
describe('OrcidQueueComponent test suite', () => {
|
||||||
|
let component: OrcidQueueComponent;
|
||||||
|
let fixture: ComponentFixture<OrcidQueueComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let orcidQueueService: OrcidQueueService;
|
||||||
|
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||||
|
|
||||||
|
const testProfileItemId = 'test-owner-id';
|
||||||
|
|
||||||
|
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{
|
||||||
|
value: 'test person'
|
||||||
|
}],
|
||||||
|
'dspace.entity.type': [{
|
||||||
|
'value': 'Person'
|
||||||
|
}],
|
||||||
|
'dspace.object.owner': [{
|
||||||
|
'value': 'test person',
|
||||||
|
'language': null,
|
||||||
|
'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff',
|
||||||
|
'confidence': 600,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.authenticated': [{
|
||||||
|
'value': '2022-06-10T15:15:12.952872',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}],
|
||||||
|
'dspace.orcid.scope': [{
|
||||||
|
'value': '/authenticate',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}, {
|
||||||
|
'value': '/read-limited',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 1
|
||||||
|
}, {
|
||||||
|
'value': '/activities/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 2
|
||||||
|
}, {
|
||||||
|
'value': '/person/update',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 3
|
||||||
|
}],
|
||||||
|
'person.identifier.orcid': [{
|
||||||
|
'value': 'orcid-id',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function orcidQueueElement(id: number) {
|
||||||
|
return Object.assign(new OrcidQueue(), {
|
||||||
|
'id': id,
|
||||||
|
'profileItemId': testProfileItemId,
|
||||||
|
'entityId': `test-entity-${id}`,
|
||||||
|
'description': `test description ${id}`,
|
||||||
|
'recordType': 'Publication',
|
||||||
|
'operation': 'INSERT',
|
||||||
|
'type': 'orcidqueue',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const orcidQueueElements = [orcidQueueElement(1), orcidQueueElement(2)];
|
||||||
|
|
||||||
|
const orcidQueueServiceSpy = jasmine.createSpyObj('orcidQueueService', ['searchByProfileItemId', 'clearFindByProfileItemRequests']);
|
||||||
|
orcidQueueServiceSpy.searchByProfileItemId.and.returnValue(createSuccessfulRemoteDataObject$<PaginatedList<OrcidQueue>>(createPaginatedList<OrcidQueue>(orcidQueueElements)));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
|
||||||
|
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl')
|
||||||
|
});
|
||||||
|
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [OrcidQueueComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: OrcidAuthService, useValue: orcidAuthService },
|
||||||
|
{ provide: OrcidQueueService, useValue: orcidQueueServiceSpy },
|
||||||
|
{ provide: OrcidHistoryDataService, useValue: {} },
|
||||||
|
{ provide: PaginationService, useValue: new PaginationServiceStub() },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
orcidQueueService = TestBed.inject(OrcidQueueService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(OrcidQueueComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.item = mockItemLinkedToOrcid;
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the ORCID queue elements', () => {
|
||||||
|
const table = debugElement.queryAll(By.css('[data-test="orcidQueueElementRow"]'));
|
||||||
|
expect(table.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,302 @@
|
|||||||
|
import { Component, Input, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||||
|
import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { OrcidHistory } from '../../../core/orcid/model/orcid-history.model';
|
||||||
|
import { OrcidQueue } from '../../../core/orcid/model/orcid-queue.model';
|
||||||
|
import { OrcidHistoryDataService } from '../../../core/orcid/orcid-history-data.service';
|
||||||
|
import { OrcidQueueService } from '../../../core/orcid/orcid-queue.service';
|
||||||
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-orcid-queue',
|
||||||
|
templateUrl: './orcid-queue.component.html',
|
||||||
|
styleUrls: ['./orcid-queue.component.scss']
|
||||||
|
})
|
||||||
|
export class OrcidQueueComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item for which showing the orcid settings
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination config used to display the list
|
||||||
|
*/
|
||||||
|
public paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'oqp',
|
||||||
|
pageSize: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if results are loading
|
||||||
|
*/
|
||||||
|
public processing$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of orcid queue records
|
||||||
|
*/
|
||||||
|
private list$: BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>> = new BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>>({} as any);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
* @type {AlertType}
|
||||||
|
*/
|
||||||
|
AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(private orcidAuthService: OrcidAuthService,
|
||||||
|
private orcidQueueService: OrcidQueueService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
private paginationService: PaginationService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private orcidHistoryService: OrcidHistoryDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updateList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
|
||||||
|
this.updateList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve queue list
|
||||||
|
*/
|
||||||
|
updateList() {
|
||||||
|
this.subs.push(
|
||||||
|
this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe(
|
||||||
|
debounceTime(100),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
tap(() => this.processing$.next(true)),
|
||||||
|
switchMap((config: PaginationComponentOptions) => this.orcidQueueService.searchByProfileItemId(this.item.id, config, false)),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((result: RemoteData<PaginatedList<OrcidQueue>>) => {
|
||||||
|
this.processing$.next(false);
|
||||||
|
this.list$.next(result);
|
||||||
|
this.orcidQueueService.clearFindByProfileItemRequests();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of orcid queue records
|
||||||
|
*/
|
||||||
|
getList(): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
|
||||||
|
return this.list$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the icon class for the queue object type
|
||||||
|
*
|
||||||
|
* @param orcidQueue The OrcidQueue object
|
||||||
|
*/
|
||||||
|
getIconClass(orcidQueue: OrcidQueue): string {
|
||||||
|
if (!orcidQueue.recordType) {
|
||||||
|
return 'fa fa-user';
|
||||||
|
}
|
||||||
|
switch (orcidQueue.recordType.toLowerCase()) {
|
||||||
|
case 'publication':
|
||||||
|
return 'fas fa-book';
|
||||||
|
case 'project':
|
||||||
|
return 'fas fa-wallet';
|
||||||
|
case 'education':
|
||||||
|
return 'fas fa-school';
|
||||||
|
case 'affiliation':
|
||||||
|
return 'fas fa-university';
|
||||||
|
case 'country':
|
||||||
|
return 'fas fa-globe-europe';
|
||||||
|
case 'external_ids':
|
||||||
|
case 'researcher_urls':
|
||||||
|
return 'fas fa-external-link-alt';
|
||||||
|
default:
|
||||||
|
return 'fa fa-user';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the icon tooltip message for the queue object type
|
||||||
|
*
|
||||||
|
* @param orcidQueue The OrcidQueue object
|
||||||
|
*/
|
||||||
|
getIconTooltip(orcidQueue: OrcidQueue): string {
|
||||||
|
if (!orcidQueue.recordType) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'person.page.orcid.sync-queue.tooltip.' + orcidQueue.recordType.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the icon tooltip message for the queue object operation
|
||||||
|
*
|
||||||
|
* @param orcidQueue The OrcidQueue object
|
||||||
|
*/
|
||||||
|
getOperationTooltip(orcidQueue: OrcidQueue): string {
|
||||||
|
if (!orcidQueue.operation) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'person.page.orcid.sync-queue.tooltip.' + orcidQueue.operation.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the icon class for the queue object operation
|
||||||
|
*
|
||||||
|
* @param orcidQueue The OrcidQueue object
|
||||||
|
*/
|
||||||
|
getOperationClass(orcidQueue: OrcidQueue): string {
|
||||||
|
|
||||||
|
if (!orcidQueue.operation) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (orcidQueue.operation.toLowerCase()) {
|
||||||
|
case 'insert':
|
||||||
|
return 'fas fa-plus';
|
||||||
|
case 'update':
|
||||||
|
return 'fas fa-edit';
|
||||||
|
case 'delete':
|
||||||
|
return 'fas fa-trash-alt';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discard a queue entry from the synchronization
|
||||||
|
*
|
||||||
|
* @param orcidQueue The OrcidQueue object to discard
|
||||||
|
*/
|
||||||
|
discardEntry(orcidQueue: OrcidQueue) {
|
||||||
|
this.processing$.next(true);
|
||||||
|
this.subs.push(this.orcidQueueService.deleteById(orcidQueue.id).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((remoteData) => {
|
||||||
|
this.processing$.next(false);
|
||||||
|
if (remoteData.isSuccess) {
|
||||||
|
this.notificationsService.success(this.translateService.get('person.page.orcid.sync-queue.discard.success'));
|
||||||
|
this.updateList();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.discard.error'));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a queue entry to orcid for the synchronization
|
||||||
|
*
|
||||||
|
* @param orcidQueue The OrcidQueue object to synchronize
|
||||||
|
*/
|
||||||
|
send(orcidQueue: OrcidQueue) {
|
||||||
|
this.processing$.next(true);
|
||||||
|
this.subs.push(this.orcidHistoryService.sendToORCID(orcidQueue).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((remoteData) => {
|
||||||
|
this.processing$.next(false);
|
||||||
|
if (remoteData.isSuccess) {
|
||||||
|
this.handleOrcidHistoryRecordCreation(remoteData.payload);
|
||||||
|
} else if (remoteData.statusCode === 422) {
|
||||||
|
this.handleValidationErrors(remoteData);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.error'));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the error message for Unauthorized response
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getUnauthorizedErrorContent(): Observable<string> {
|
||||||
|
return this.orcidAuthService.getOrcidAuthorizeUrl(this.item).pipe(
|
||||||
|
switchMap((authorizeUrl) => this.translateService.get(
|
||||||
|
'person.page.orcid.sync-queue.send.unauthorized-error.content',
|
||||||
|
{ orcid: authorizeUrl }
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage notification by response
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private handleOrcidHistoryRecordCreation(orcidHistory: OrcidHistory) {
|
||||||
|
switch (orcidHistory.status) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
|
case 204:
|
||||||
|
this.notificationsService.success(this.translateService.get('person.page.orcid.sync-queue.send.success'));
|
||||||
|
this.updateList();
|
||||||
|
break;
|
||||||
|
case 400:
|
||||||
|
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.bad-request-error'), null, { timeOut: 0 });
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
combineLatest([
|
||||||
|
this.translateService.get('person.page.orcid.sync-queue.send.unauthorized-error.title'),
|
||||||
|
this.getUnauthorizedErrorContent()],
|
||||||
|
).subscribe(([title, content]) => {
|
||||||
|
this.notificationsService.error(title, content, { timeOut: 0 }, true);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
this.notificationsService.warning(this.translateService.get('person.page.orcid.sync-queue.send.not-found-warning'));
|
||||||
|
break;
|
||||||
|
case 409:
|
||||||
|
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.conflict-error'), null, { timeOut: 0 });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.error'), null, { timeOut: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage validation errors
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private handleValidationErrors(remoteData: RemoteData<OrcidHistory>) {
|
||||||
|
const translations = [this.translateService.get('person.page.orcid.sync-queue.send.validation-error')];
|
||||||
|
const errorMessage = remoteData.errorMessage;
|
||||||
|
if (errorMessage && errorMessage.indexOf('Error codes:') > 0) {
|
||||||
|
errorMessage.substring(errorMessage.indexOf(':') + 1).trim().split(',')
|
||||||
|
.forEach((error) => translations.push(this.translateService.get('person.page.orcid.sync-queue.send.validation-error.' + error)));
|
||||||
|
}
|
||||||
|
combineLatest(translations).subscribe((messages) => {
|
||||||
|
const title = messages.shift();
|
||||||
|
const content = '<ul>' + messages.map((message) => `<li>${message}</li>`).join('') + '</ul>';
|
||||||
|
this.notificationsService.error(title, content, { timeOut: 0 }, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.list$ = null;
|
||||||
|
this.subs.filter((subscription) => hasValue(subscription))
|
||||||
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -54,6 +54,7 @@ export class ActivatedRouteStub {
|
|||||||
get snapshot() {
|
get snapshot() {
|
||||||
return {
|
return {
|
||||||
params: this.testParams,
|
params: this.testParams,
|
||||||
|
paramMap: convertToParamMap(this.params),
|
||||||
queryParamMap: convertToParamMap(this.testParams)
|
queryParamMap: convertToParamMap(this.testParams)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3735,6 +3735,8 @@
|
|||||||
|
|
||||||
"submission.import-external.source.wos": "Web Of Science",
|
"submission.import-external.source.wos": "Web Of Science",
|
||||||
|
|
||||||
|
"submission.import-external.source.orcidWorks": "ORCID",
|
||||||
|
|
||||||
"submission.import-external.source.epo": "European Patent Office (EPO)",
|
"submission.import-external.source.epo": "European Patent Office (EPO)",
|
||||||
|
|
||||||
"submission.import-external.source.loading": "Loading ...",
|
"submission.import-external.source.loading": "Loading ...",
|
||||||
@@ -4510,8 +4512,6 @@
|
|||||||
|
|
||||||
"researcherprofile.success.claim.title" : "Success",
|
"researcherprofile.success.claim.title" : "Success",
|
||||||
|
|
||||||
"person.page.orcid": "ORCID",
|
|
||||||
|
|
||||||
"person.page.orcid.create": "Create an ORCID ID",
|
"person.page.orcid.create": "Create an ORCID ID",
|
||||||
|
|
||||||
"person.page.orcid.granted-authorizations": "Granted authorizations",
|
"person.page.orcid.granted-authorizations": "Granted authorizations",
|
||||||
@@ -4580,7 +4580,11 @@
|
|||||||
|
|
||||||
"person.page.orcid.sync-queue.empty-message": "The ORCID queue registry is empty",
|
"person.page.orcid.sync-queue.empty-message": "The ORCID queue registry is empty",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.description" : "Description",
|
"person.page.orcid.sync-queue.table.header.type" : "Type",
|
||||||
|
|
||||||
|
"person.page.orcid.sync-queue.table.header.description" : "Description",
|
||||||
|
|
||||||
|
"person.page.orcid.sync-queue.table.header.action" : "Action",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.description.affiliation": "Affiliations",
|
"person.page.orcid.sync-queue.description.affiliation": "Affiliations",
|
||||||
|
|
||||||
@@ -4606,7 +4610,7 @@
|
|||||||
|
|
||||||
"person.page.orcid.sync-queue.tooltip.publication": "Publication",
|
"person.page.orcid.sync-queue.tooltip.publication": "Publication",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.tooltip.funding": "Funding",
|
"person.page.orcid.sync-queue.tooltip.project": "Project",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.tooltip.affiliation": "Affiliation",
|
"person.page.orcid.sync-queue.tooltip.affiliation": "Affiliation",
|
||||||
|
|
||||||
@@ -4648,21 +4652,25 @@
|
|||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.title.required": "The title is required",
|
"person.page.orcid.sync-queue.send.validation-error.title.required": "The title is required",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.type.required": "The type is required",
|
"person.page.orcid.sync-queue.send.validation-error.type.required": "The dc.type is required",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.start-date.required": "The start date is required",
|
"person.page.orcid.sync-queue.send.validation-error.start-date.required": "The start date is required",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.funder.required": "The funder is required",
|
"person.page.orcid.sync-queue.send.validation-error.funder.required": "The funder is required",
|
||||||
|
|
||||||
|
"person.page.orcid.sync-queue.send.validation-error.country.invalid": "Invalid 2 digits ISO 3166 country",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.organization.required": "The organization is required",
|
"person.page.orcid.sync-queue.send.validation-error.organization.required": "The organization is required",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.organization.name-required": "The organization's name is required",
|
"person.page.orcid.sync-queue.send.validation-error.organization.name-required": "The organization's name is required",
|
||||||
|
|
||||||
|
"person.page.orcid.sync-queue.send.validation-error.publication.date-invalid" : "The publication date must be one year after 1900",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.organization.address-required": "The organization to be sent requires an address",
|
"person.page.orcid.sync-queue.send.validation-error.organization.address-required": "The organization to be sent requires an address",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.organization.city-required": "The address of the organization to be sent requires a city",
|
"person.page.orcid.sync-queue.send.validation-error.organization.city-required": "The address of the organization to be sent requires a city",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.organization.country-required": "The address of the organization to be sent requires a country",
|
"person.page.orcid.sync-queue.send.validation-error.organization.country-required": "The address of the organization to be sent requires a valid 2 digits ISO 3166 country",
|
||||||
|
|
||||||
"person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.required": "An identifier to disambiguate organizations is required. Supported ids are GRID, Ringgold, Legal Entity identifiers (LEIs) and Crossref Funder Registry identifiers",
|
"person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.required": "An identifier to disambiguate organizations is required. Supported ids are GRID, Ringgold, Legal Entity identifiers (LEIs) and Crossref Funder Registry identifiers",
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user