mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into feature-relationship-versioning
This commit is contained in:
@@ -25,7 +25,7 @@ services:
|
||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||
# 2. Then, run database migration to init database tables
|
||||
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
||||
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
||||
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate
|
||||
/dspace/bin/dspace database migrate ignored
|
||||
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||
|
@@ -46,14 +46,14 @@ services:
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||
# 2. Then, run database migration to init database tables
|
||||
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||
# 3. Finally, start Tomcat
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate
|
||||
/dspace/bin/dspace database migrate ignored
|
||||
catalina.sh run
|
||||
# DSpace database container
|
||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||
|
@@ -107,6 +107,8 @@ export function getPageInternalServerErrorRoute() {
|
||||
return `/${INTERNAL_SERVER_ERROR}`;
|
||||
}
|
||||
|
||||
export const ERROR_PAGE = 'error';
|
||||
|
||||
export const INFO_MODULE_PATH = 'info';
|
||||
export function getInfoModulePath() {
|
||||
return `/${INFO_MODULE_PATH}`;
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
ACCESS_CONTROL_MODULE_PATH,
|
||||
ADMIN_MODULE_PATH,
|
||||
BITSTREAM_MODULE_PATH,
|
||||
ERROR_PAGE,
|
||||
FORBIDDEN_PATH,
|
||||
FORGOT_PASSWORD_PATH,
|
||||
HEALTH_PAGE_PATH,
|
||||
@@ -38,11 +39,13 @@ import {
|
||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
import { MenuResolver } from './menu.resolver';
|
||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
||||
{ path: ERROR_PAGE , component: ThemedPageErrorComponent },
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthBlockingGuard],
|
||||
|
@@ -4,5 +4,6 @@ export enum AuthMethodType {
|
||||
Ldap = 'ldap',
|
||||
Ip = 'ip',
|
||||
X509 = 'x509',
|
||||
Oidc = 'oidc'
|
||||
Oidc = 'oidc',
|
||||
Orcid = 'orcid'
|
||||
}
|
||||
|
@@ -34,6 +34,11 @@ export class AuthMethod {
|
||||
this.location = location;
|
||||
break;
|
||||
}
|
||||
case 'orcid': {
|
||||
this.authMethodType = AuthMethodType.Orcid;
|
||||
this.location = location;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
|
@@ -38,7 +38,7 @@ import { SubmissionSectionModel } from './config/models/config-submission-sectio
|
||||
import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model';
|
||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||
import { coreEffects } from './core.effects';
|
||||
import { coreReducers} from './core.reducers';
|
||||
import { coreReducers } from './core.reducers';
|
||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||
import { CollectionDataService } from './data/collection-data.service';
|
||||
import { CommunityDataService } from './data/community-data.service';
|
||||
@@ -132,11 +132,15 @@ import { Feature } from './shared/feature.model';
|
||||
import { Authorization } from './shared/authorization.model';
|
||||
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
||||
import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service';
|
||||
import { SiteAdministratorGuard } from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
SiteAdministratorGuard
|
||||
} from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import { Registration } from './shared/registration.model';
|
||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||
import { DsDynamicTypeBindRelationService } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||
import {
|
||||
DsDynamicTypeBindRelationService
|
||||
} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
|
||||
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||
|
@@ -29,4 +29,5 @@ export enum FeatureID {
|
||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||
CanSendFeedback = 'canSendFeedback',
|
||||
CanClaimItem = 'canClaimItem',
|
||||
CanSynchronizeWithORCID = 'canSynchronizeWithORCID'
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import { RequestService } from '../data/request.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { buildPaginatedList } from '../data/paginated-list.model';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createNoContentRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
@@ -22,10 +23,15 @@ import { ResearcherProfileService } from './researcher-profile.service';
|
||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||
import { ResearcherProfile } from './model/researcher-profile.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { ReplaceOperation } from 'fast-json-patch';
|
||||
import { AddOperation, RemoveOperation, ReplaceOperation } from 'fast-json-patch';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
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';
|
||||
|
||||
describe('ResearcherProfileService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -36,6 +42,9 @@ describe('ResearcherProfileService', () => {
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
let configurationDataService: ConfigurationDataService;
|
||||
let nativeWindowService: NativeWindowRefMock;
|
||||
let routerStub: any;
|
||||
|
||||
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||
@@ -86,6 +95,113 @@ describe('ResearcherProfileService', () => {
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/profiles`;
|
||||
const endpointURLWithEmbed = 'https://rest.api/rest/api/profiles?embed=item';
|
||||
const sourceUri = `https://rest.api/rest/api/external-source/profile`;
|
||||
@@ -132,12 +248,17 @@ describe('ResearcherProfileService', () => {
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const routerStub: any = new RouterMock();
|
||||
routerStub = new RouterMock();
|
||||
const itemService = jasmine.createSpyObj('ItemService', {
|
||||
findByHref: jasmine.createSpy('findByHref')
|
||||
});
|
||||
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||
});
|
||||
nativeWindowService = new NativeWindowRefMock();
|
||||
|
||||
service = new ResearcherProfileService(
|
||||
nativeWindowService,
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
@@ -146,7 +267,8 @@ describe('ResearcherProfileService', () => {
|
||||
http,
|
||||
routerStub,
|
||||
comparator,
|
||||
itemService
|
||||
itemService,
|
||||
configurationDataService
|
||||
);
|
||||
serviceAsAny = service;
|
||||
|
||||
@@ -271,7 +393,7 @@ describe('ResearcherProfileService', () => {
|
||||
});
|
||||
|
||||
describe('createFromExternalSource', () => {
|
||||
let patchSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
@@ -293,4 +415,162 @@ 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', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
});
|
||||
|
||||
it('should call patch method properly', () => {
|
||||
scheduler.schedule(() => service.updateByOrcidOperations(researcherProfile, []).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
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,33 +1,41 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ReplaceOperation } from 'fast-json-patch';
|
||||
import { Observable } from 'rxjs';
|
||||
import { find, map } from 'rxjs/operators';
|
||||
|
||||
import { AddOperation, Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { find, 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 { ConfigurationDataService } from '../data/configuration-data.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 { RequestService } from '../data/request.service';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { getAllCompletedRemoteData, getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import {
|
||||
getAllCompletedRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../shared/operators';
|
||||
import { ResearcherProfile } from './model/researcher-profile.model';
|
||||
import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
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.
|
||||
@@ -56,11 +64,12 @@ class ResearcherProfileServiceImpl extends DataService<ResearcherProfile> {
|
||||
@dataService(RESEARCHER_PROFILE)
|
||||
export class ResearcherProfileService {
|
||||
|
||||
dataService: ResearcherProfileServiceImpl;
|
||||
protected dataService: ResearcherProfileServiceImpl;
|
||||
|
||||
responseMsToLive: number = 10 * 1000;
|
||||
protected responseMsToLive: number = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
@@ -69,7 +78,8 @@ export class ResearcherProfileService {
|
||||
protected http: HttpClient,
|
||||
protected router: Router,
|
||||
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>,
|
||||
protected itemService: ItemDataService) {
|
||||
protected itemService: ItemDataService,
|
||||
protected configurationService: ConfigurationDataService) {
|
||||
|
||||
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService,
|
||||
notificationsService, http, comparator);
|
||||
@@ -112,6 +122,20 @@ export class ResearcherProfileService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a researcher profile by its own related item
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
public findByRelatedItem(item: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||
const profileId = item.firstMetadata('dspace.object.owner')?.authority;
|
||||
if (isEmpty(profileId)) {
|
||||
return createFailedRemoteDataObject$();
|
||||
} else {
|
||||
return this.findById(profileId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the item id related to the given researcher profile.
|
||||
*
|
||||
@@ -141,6 +165,98 @@ export class ResearcherProfileService {
|
||||
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
|
||||
* @param sourceUri URI of source item of researcher profile.
|
||||
@@ -164,4 +280,40 @@ export class ResearcherProfileService {
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId, followLink('item'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update researcher profile by patch orcid operation
|
||||
*
|
||||
* @param researcherProfile
|
||||
* @param operations
|
||||
*/
|
||||
public updateByOrcidOperations(researcherProfile: ResearcherProfile, operations: Operation[]): Observable<RemoteData<ResearcherProfile>> {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="getTitleMetadataValues()" [separator]="', '"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object" class="mr-2"></ds-dso-page-orcid-button>
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
|
@@ -50,3 +50,4 @@ export const ITEM_EDIT_PATH = 'edit';
|
||||
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
|
||||
export const ITEM_VERSION_PATH = 'version';
|
||||
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
||||
export const ORCID_PATH = 'orcid';
|
||||
|
@@ -7,15 +7,19 @@ import { VersionResolver } from './version-page/version.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
import { LinkService } from '../core/cache/builders/link.service';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||
import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
import {
|
||||
BitstreamRequestACopyPageComponent
|
||||
} from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||
import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -50,6 +54,11 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
{
|
||||
path: REQUEST_COPY_MODULE_PATH,
|
||||
component: BitstreamRequestACopyPageComponent,
|
||||
},
|
||||
{
|
||||
path: ORCID_PATH,
|
||||
component: OrcidPageComponent,
|
||||
canActivate: [AuthenticatedGuard, OrcidPageGuard]
|
||||
}
|
||||
],
|
||||
data: {
|
||||
@@ -88,6 +97,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
LinkService,
|
||||
ItemPageAdministratorGuard,
|
||||
VersionResolver,
|
||||
OrcidPageGuard
|
||||
]
|
||||
|
||||
})
|
||||
|
@@ -6,11 +6,19 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { ItemPageComponent } from './simple/item-page.component';
|
||||
import { ItemPageRoutingModule } from './item-page-routing.module';
|
||||
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
|
||||
import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component';
|
||||
import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component';
|
||||
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||
import {
|
||||
ItemPageAuthorFieldComponent
|
||||
} from './simple/field-components/specific-field/author/item-page-author-field.component';
|
||||
import {
|
||||
ItemPageDateFieldComponent
|
||||
} from './simple/field-components/specific-field/date/item-page-date-field.component';
|
||||
import {
|
||||
ItemPageAbstractFieldComponent
|
||||
} from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
||||
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||
import {
|
||||
ItemPageTitleFieldComponent
|
||||
} from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||
import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component';
|
||||
import { CollectionsComponent } from './field-components/collections/collections.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||
@@ -20,7 +28,9 @@ import { ItemComponent } from './simple/item-types/shared/item.component';
|
||||
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
||||
import {
|
||||
AbstractIncrementalListComponent
|
||||
} from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
||||
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
@@ -34,6 +44,10 @@ import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.componen
|
||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
|
||||
import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component';
|
||||
import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component';
|
||||
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
|
||||
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -67,6 +81,9 @@ const DECLARATIONS = [
|
||||
MediaViewerImageComponent,
|
||||
MiradorViewerComponent,
|
||||
VersionPageComponent,
|
||||
OrcidPageComponent,
|
||||
OrcidAuthComponent,
|
||||
OrcidSyncSettingsComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -79,6 +96,7 @@ const DECLARATIONS = [
|
||||
JournalEntitiesModule.withEntryComponents(),
|
||||
ResearchEntitiesModule.withEntryComponents(),
|
||||
NgxGalleryModule,
|
||||
NgbAccordionModule
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS,
|
||||
|
@@ -0,0 +1,84 @@
|
||||
<div class="container mb-5">
|
||||
<h2>{{'person.orcid.registry.auth' | translate}}</h2>
|
||||
<ng-container *ngIf="(isLinkedToOrcid() | async); then orcidLinked; else orcidNotLinked"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #orcidLinked>
|
||||
<div data-test="orcidLinked">
|
||||
<div class="row">
|
||||
<div *ngIf="(hasOrcidAuthorizations() | async)" class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container p-0">
|
||||
<ul>
|
||||
<li *ngFor="let auth of (getOrcidAuthorizations() | async)" data-test="orcidAuthorization">
|
||||
{{getAuthorizationDescription(auth) | translate}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<ds-alert *ngIf="!(hasMissingOrcidAuthorizations() | async)" [type]="'alert-success'" data-test="noMissingOrcidAuthorizations">
|
||||
{{'person.page.orcid.no-missing-authorizations-message' | translate}}
|
||||
</ds-alert>
|
||||
<ds-alert *ngIf="(hasMissingOrcidAuthorizations() | async)" [type]="'alert-warning'" data-test="missingOrcidAuthorizations">
|
||||
{{'person.page.orcid.missing-authorizations-message' | translate}}
|
||||
<ul>
|
||||
<li *ngFor="let auth of (getMissingOrcidAuthorizations() | async)" data-test="missingOrcidAuthorization">
|
||||
{{getAuthorizationDescription(auth) | translate }}
|
||||
</li>
|
||||
</ul>
|
||||
</ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-alert *ngIf="(onlyAdminCanDisconnectProfileFromOrcid() | async) && !(ownerCanDisconnectProfileFromOrcid() | async)"
|
||||
[type]="'alert-warning'" data-test="unlinkOnlyAdmin">
|
||||
{{ 'person.page.orcid.remove-orcid-message' | translate}}
|
||||
</ds-alert>
|
||||
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
|
||||
[disabled]="(unlinkProcessing | async)">
|
||||
<span *ngIf="!(unlinkProcessing | async)"><i
|
||||
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
|
||||
<span *ngIf="(unlinkProcessing | async)"><i
|
||||
class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(hasMissingOrcidAuthorizations() | async)" type="submit"
|
||||
class="btn btn-primary float-right" (click)="linkOrcid()">
|
||||
<span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #orcidNotLinked>
|
||||
<div data-test="orcidNotLinked">
|
||||
<div class="row">
|
||||
<div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div>
|
||||
<div class="col">
|
||||
<ds-alert [type]="'alert-info'">{{ getOrcidNotLinkedMessage() | async }}</ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button class="btn btn-primary float-right" (click)="linkOrcid()">
|
||||
<i class="fas fa-link"></i>
|
||||
{{'person.page.orcid.link' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@@ -0,0 +1,336 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { of } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||
import { OrcidAuthComponent } from './orcid-auth.component';
|
||||
import { NativeWindowService } from '../../../core/services/window.service';
|
||||
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||
|
||||
describe('OrcidAuthComponent test suite', () => {
|
||||
let comp: OrcidAuthComponent;
|
||||
let fixture: ComponentFixture<OrcidAuthComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||
let nativeWindowRef;
|
||||
let notificationsService;
|
||||
|
||||
const orcidScopes = [
|
||||
'/authenticate',
|
||||
'/read-limited',
|
||||
'/activities/update',
|
||||
'/person/update'
|
||||
];
|
||||
|
||||
const partialOrcidScopes = [
|
||||
'/authenticate',
|
||||
'/read-limited',
|
||||
];
|
||||
|
||||
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
||||
getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'),
|
||||
getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'),
|
||||
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'),
|
||||
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||
onlyAdminCanDisconnectProfileFromOrcid: jasmine.createSpy('onlyAdminCanDisconnectProfileFromOrcid'),
|
||||
ownerCanDisconnectProfileFromOrcid: jasmine.createSpy('ownerCanDisconnectProfileFromOrcid'),
|
||||
unlinkOrcidByItem: jasmine.createSpy('unlinkOrcidByItem')
|
||||
});
|
||||
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbAccordionModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [OrcidAuthComponent],
|
||||
providers: [
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
{ provide: ResearcherProfileService, useValue: researcherProfileService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(OrcidAuthComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
fixture = TestBed.createComponent(OrcidAuthComponent);
|
||||
comp = fixture.componentInstance;
|
||||
researcherProfileService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
|
||||
}));
|
||||
|
||||
describe('when orcid profile is not linked', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemUnlinkedToOrcid;
|
||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(false);
|
||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
researcherProfileService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl'));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', fakeAsync(() => {
|
||||
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||
expect(orcidLinked).toBeFalsy();
|
||||
expect(orcidNotLinked).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should change location on link', () => {
|
||||
nativeWindowRef = (comp as any)._window;
|
||||
scheduler.schedule(() => comp.linkOrcid());
|
||||
scheduler.flush();
|
||||
|
||||
expect(nativeWindowRef.nativeWindow.location.href).toBe('oarcidUrl');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when orcid profile is linked', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
notificationsService = (comp as any).notificationsService;
|
||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
}));
|
||||
|
||||
describe('and unlink is successfully', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile()));
|
||||
spyOn(comp.unlink, 'emit');
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should show success notification', () => {
|
||||
scheduler.schedule(() => comp.unlinkOrcid());
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(comp.unlink.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and unlink is failed', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should show success notification', () => {
|
||||
scheduler.schedule(() => comp.unlinkOrcid());
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and has orcid authorization scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', fakeAsync(() => {
|
||||
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||
expect(orcidLinked).toBeTruthy();
|
||||
expect(orcidNotLinked).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should display orcid authorizations', fakeAsync(() => {
|
||||
const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]'));
|
||||
const noMissingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="noMissingOrcidAuthorizations"]'));
|
||||
const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]'));
|
||||
|
||||
expect(orcidAuthorizations).toBeTruthy();
|
||||
expect(noMissingOrcidAuthorizations).toBeTruthy();
|
||||
expect(orcidAuthorizationsList.length).toBe(4);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and has missing orcid authorization scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', fakeAsync(() => {
|
||||
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||
expect(orcidLinked).toBeTruthy();
|
||||
expect(orcidNotLinked).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should display orcid authorizations', fakeAsync(() => {
|
||||
const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]'));
|
||||
const missingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="missingOrcidAuthorizations"]'));
|
||||
const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]'));
|
||||
const missingOrcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="missingOrcidAuthorization"]'));
|
||||
|
||||
expect(orcidAuthorizations).toBeTruthy();
|
||||
expect(missingOrcidAuthorizations).toBeTruthy();
|
||||
expect(orcidAuthorizationsList.length).toBe(2);
|
||||
expect(missingOrcidAuthorizationsList.length).toBe(2);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and only admin can unlink scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should display warning panel', fakeAsync(() => {
|
||||
const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]'));
|
||||
const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]'));
|
||||
expect(unlinkOnlyAdmin).toBeTruthy();
|
||||
expect(unlinkOwner).toBeFalsy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('and owner can unlink scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
researcherProfileService.isLinkedToOrcid.and.returnValue(true);
|
||||
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should display warning panel', fakeAsync(() => {
|
||||
const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]'));
|
||||
const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]'));
|
||||
expect(unlinkOnlyAdmin).toBeFalsy();
|
||||
expect(unlinkOwner).toBeTruthy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
219
src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
Normal file
219
src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-orcid-auth',
|
||||
templateUrl: './orcid-auth.component.html',
|
||||
styleUrls: ['./orcid-auth.component.scss']
|
||||
})
|
||||
export class OrcidAuthComponent implements OnInit, OnChanges {
|
||||
|
||||
/**
|
||||
* The item for which showing the orcid settings
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The list of exposed orcid authorization scopes for the orcid profile
|
||||
*/
|
||||
profileAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
|
||||
/**
|
||||
* The list of all orcid authorization scopes missing in the orcid profile
|
||||
*/
|
||||
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
|
||||
/**
|
||||
* The list of all orcid authorization scopes available
|
||||
*/
|
||||
orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
|
||||
/**
|
||||
* A boolean representing if unlink operation is processing
|
||||
*/
|
||||
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A boolean representing if orcid profile is linked
|
||||
*/
|
||||
private isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A boolean representing if only admin can disconnect orcid profile
|
||||
*/
|
||||
private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A boolean representing if owner can disconnect orcid profile
|
||||
*/
|
||||
private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* An event emitted when orcid profile is unliked successfully
|
||||
*/
|
||||
@Output() unlink: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private researcherProfileService: ResearcherProfileService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.researcherProfileService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
|
||||
this.orcidAuthorizationScopes.next(scopes);
|
||||
this.initOrcidAuthSettings();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
|
||||
this.initOrcidAuthSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the list of exposed orcid authorization scopes for the orcid profile has values
|
||||
*/
|
||||
hasOrcidAuthorizations(): Observable<boolean> {
|
||||
return this.profileAuthorizationScopes.asObservable().pipe(
|
||||
map((scopes: string[]) => scopes.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of exposed orcid authorization scopes for the orcid profile
|
||||
*/
|
||||
getOrcidAuthorizations(): Observable<string[]> {
|
||||
return this.profileAuthorizationScopes.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the list of exposed orcid authorization scopes for the orcid profile has values
|
||||
*/
|
||||
hasMissingOrcidAuthorizations(): Observable<boolean> {
|
||||
return this.missingAuthorizationScopes.asObservable().pipe(
|
||||
map((scopes: string[]) => scopes.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of exposed orcid authorization scopes for the orcid profile
|
||||
*/
|
||||
getMissingOrcidAuthorizations(): Observable<string[]> {
|
||||
return this.profileAuthorizationScopes.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean representing if orcid profile is linked
|
||||
*/
|
||||
isLinkedToOrcid(): Observable<boolean> {
|
||||
return this.isOrcidLinked$.asObservable();
|
||||
}
|
||||
|
||||
getOrcidNotLinkedMessage(): Observable<string> {
|
||||
const orcid = this.item.firstMetadataValue('person.identifier.orcid');
|
||||
if (orcid) {
|
||||
return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid });
|
||||
} else {
|
||||
return this.translateService.get('person.page.orcid.no-orcid-message');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get label for a given orcid authorization scope
|
||||
*
|
||||
* @param scope
|
||||
*/
|
||||
getAuthorizationDescription(scope: string) {
|
||||
return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean representing if only admin can disconnect orcid profile
|
||||
*/
|
||||
onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||
return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean representing if owner can disconnect orcid profile
|
||||
*/
|
||||
ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||
return this.ownerCanDisconnectProfileFromOrcid$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Link existing person profile with orcid
|
||||
*/
|
||||
linkOrcid(): void {
|
||||
this.researcherProfileService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
|
||||
this._window.nativeWindow.location.href = authorizeUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink existing person profile from orcid
|
||||
*/
|
||||
unlinkOrcid(): void {
|
||||
this.unlinkProcessing.next(true);
|
||||
this.researcherProfileService.unlinkOrcidByItem(this.item).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
||||
this.unlinkProcessing.next(false);
|
||||
if (remoteData.isSuccess) {
|
||||
this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success'));
|
||||
this.unlink.emit();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize all Orcid authentication settings
|
||||
* @private
|
||||
*/
|
||||
private initOrcidAuthSettings(): void {
|
||||
|
||||
this.setOrcidAuthorizationsFromItem();
|
||||
|
||||
this.setMissingOrcidAuthorizations();
|
||||
|
||||
this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||
this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
|
||||
});
|
||||
|
||||
this.researcherProfileService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||
this.ownerCanDisconnectProfileFromOrcid$.next(result);
|
||||
});
|
||||
|
||||
this.isOrcidLinked$.next(this.researcherProfileService.isLinkedToOrcid(this.item));
|
||||
}
|
||||
|
||||
private setMissingOrcidAuthorizations(): void {
|
||||
const profileScopes = this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item);
|
||||
const orcidScopes = this.orcidAuthorizationScopes.value;
|
||||
const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope));
|
||||
|
||||
this.missingAuthorizationScopes.next(missingScopes);
|
||||
}
|
||||
|
||||
private setOrcidAuthorizationsFromItem(): void {
|
||||
this.profileAuthorizationScopes.next(this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item));
|
||||
}
|
||||
|
||||
}
|
18
src/app/item-page/orcid-page/orcid-page.component.html
Normal file
18
src/app/item-page/orcid-page/orcid-page.component.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div *ngIf="!(processingConnection | async) && (item | async)" class="container">
|
||||
<div class="button-row bottom mb-3">
|
||||
<div class="text-right">
|
||||
<a [routerLink]="getItemPage()" role="button" class="btn btn-outline-secondary" data-test="back-button">
|
||||
<i class="fas fa-arrow-left"></i> {{'item.orcid.return' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ds-loading *ngIf="(processingConnection | async)" [message]="'person.page.orcid.link.processing' | translate"></ds-loading>
|
||||
<div class="container" *ngIf="!(processingConnection | async) && !(connectionStatus | async)" data-test="error-box">
|
||||
<ds-alert [type]="'alert-danger'">{{'person.page.orcid.link.error.message' | translate}}</ds-alert>
|
||||
</div>
|
||||
<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-sync-setting *ngIf="isLinkedToOrcid()" [item]="(item | async)" (settingsUpdated)="updateItem()" data-test="orcid-sync-setting"></ds-orcid-sync-setting>
|
||||
</ng-container>
|
220
src/app/item-page/orcid-page/orcid-page.component.spec.ts
Normal file
220
src/app/item-page/orcid-page/orcid-page.component.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
|
||||
import { OrcidPageComponent } from './orcid-page.component';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||
|
||||
describe('OrcidPageComponent test suite', () => {
|
||||
let comp: OrcidPageComponent;
|
||||
let fixture: ComponentFixture<OrcidPageComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
let authService: jasmine.SpyObj<AuthService>;
|
||||
let routeStub: jasmine.SpyObj<ActivatedRouteStub>;
|
||||
let routeData: any;
|
||||
let itemDataService: jasmine.SpyObj<ItemDataService>;
|
||||
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||
|
||||
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||
id: 'test-id',
|
||||
visible: true,
|
||||
type: 'profile',
|
||||
_links: {
|
||||
item: {
|
||||
href: 'https://rest.api/rest/api/profiles/test-id/item'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/profiles/test-id'
|
||||
},
|
||||
}
|
||||
});
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
id: 'test-id',
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'test item'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
id: 'test-id',
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
value: 'test item'
|
||||
}
|
||||
],
|
||||
'dspace.orcid.authenticated': [
|
||||
{
|
||||
value: 'true'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: jasmine.createSpy('isAuthenticated'),
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
|
||||
routeData = {
|
||||
dso: createSuccessfulRemoteDataObject(mockItem),
|
||||
};
|
||||
|
||||
routeStub = new ActivatedRouteStub({}, routeData);
|
||||
|
||||
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
||||
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||
linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
|
||||
});
|
||||
|
||||
itemDataService = jasmine.createSpyObj('ItemDataService', {
|
||||
findById: jasmine.createSpy('findById')
|
||||
});
|
||||
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [OrcidPageComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: ResearcherProfileService, useValue: researcherProfileService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
fixture = TestBed.createComponent(OrcidPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
authService.isAuthenticated.and.returnValue(observableOf(true));
|
||||
}));
|
||||
|
||||
describe('whn has no query param', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||
expect(comp).toBeTruthy();
|
||||
expect(btn.length).toBe(1);
|
||||
expect(auth).toBeTruthy();
|
||||
expect(settings).toBeTruthy();
|
||||
expect(comp.itemId).toBe('test-id');
|
||||
});
|
||||
|
||||
it('should call isLinkedToOrcid', () => {
|
||||
comp.isLinkedToOrcid();
|
||||
|
||||
expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value);
|
||||
});
|
||||
|
||||
it('should update item', fakeAsync(() => {
|
||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||
scheduler.schedule(() => comp.updateItem());
|
||||
scheduler.flush();
|
||||
|
||||
expect(comp.item.value).toEqual(mockItemLinkedToOrcid);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when query param contains orcid code', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
spyOn(comp, 'updateItem').and.callThrough();
|
||||
routeStub.testParams = {
|
||||
code: 'orcid-code'
|
||||
};
|
||||
}));
|
||||
|
||||
describe('and linking to orcid profile is successfully', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
researcherProfileService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should call linkOrcidByItem', () => {
|
||||
expect(researcherProfileService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||
expect(comp.updateItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||
expect(comp).toBeTruthy();
|
||||
expect(btn.length).toBe(1);
|
||||
expect(auth).toBeTruthy();
|
||||
expect(settings).toBeTruthy();
|
||||
expect(comp.itemId).toBe('test-id');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('and linking to orcid profile is failed', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
researcherProfileService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should call linkOrcidByItem', () => {
|
||||
expect(researcherProfileService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||
expect(comp.updateItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||
const error = fixture.debugElement.query(By.css('[data-test="error-box"]'));
|
||||
expect(comp).toBeTruthy();
|
||||
expect(btn.length).toBe(1);
|
||||
expect(error).toBeTruthy();
|
||||
expect(auth).toBeFalsy();
|
||||
expect(settings).toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
145
src/app/item-page/orcid-page/orcid-page.component.ts
Normal file
145
src/app/item-page/orcid-page/orcid-page.component.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
|
||||
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
/**
|
||||
* A component that represents the orcid settings page
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-orcid-page',
|
||||
templateUrl: './orcid-page.component.html',
|
||||
styleUrls: ['./orcid-page.component.scss']
|
||||
})
|
||||
export class OrcidPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* A boolean representing if the connection operation with orcid profile is in progress
|
||||
*/
|
||||
connectionStatus: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* The item for which showing the orcid settings
|
||||
*/
|
||||
item: BehaviorSubject<Item> = new BehaviorSubject<Item>(null);
|
||||
|
||||
/**
|
||||
* The item id for which showing the orcid settings
|
||||
*/
|
||||
itemId: string;
|
||||
|
||||
/**
|
||||
* A boolean representing if the connection operation with orcid profile is in progress
|
||||
*/
|
||||
processingConnection: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
private authService: AuthService,
|
||||
private itemService: ItemDataService,
|
||||
private researcherProfileService: ResearcherProfileService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item for which showing the orcid settings
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
const codeParam$ = this.route.queryParamMap.pipe(
|
||||
take(1),
|
||||
map((paramMap: ParamMap) => paramMap.get('code')),
|
||||
);
|
||||
|
||||
const item$ = this.route.data.pipe(
|
||||
map((data) => data.dso as RemoteData<Item>),
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
);
|
||||
|
||||
combineLatest([codeParam$, item$]).subscribe(([codeParam, item]) => {
|
||||
this.itemId = item.id;
|
||||
/**
|
||||
* Check if code is present in the query param. If so it means this page is loaded after attempting to
|
||||
* link the person to the ORCID profile, otherwise the person is already linked to ORCID profile
|
||||
*/
|
||||
if (isNotEmpty(codeParam)) {
|
||||
this.linkProfileToOrcid(item, codeParam);
|
||||
} else {
|
||||
this.item.next(item);
|
||||
this.processingConnection.next(false);
|
||||
this.connectionStatus.next(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current item is linked to an ORCID profile.
|
||||
*
|
||||
* @returns the check result
|
||||
*/
|
||||
isLinkedToOrcid(): boolean {
|
||||
return this.researcherProfileService.isLinkedToOrcid(this.item.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route to an item's page
|
||||
*/
|
||||
getItemPage(): string {
|
||||
return getItemPageRoute(this.item.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the updated profile item
|
||||
*/
|
||||
updateItem(): void {
|
||||
this.itemService.findById(this.itemId, false).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((itemRD: RemoteData<Item>) => {
|
||||
if (itemRD.hasSucceeded) {
|
||||
this.item.next(itemRD.payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Link person item to ORCID profile by using the code received after redirect from ORCID.
|
||||
*
|
||||
* @param person The person item to link to ORCID profile
|
||||
* @param code The auth-code received from ORCID
|
||||
*/
|
||||
private linkProfileToOrcid(person: Item, code: string) {
|
||||
this.researcherProfileService.linkOrcidByItem(person, code).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((profileRD: RemoteData<ResearcherProfile>) => {
|
||||
this.processingConnection.next(false);
|
||||
if (profileRD.hasSucceeded) {
|
||||
this.connectionStatus.next(true);
|
||||
this.updateItem();
|
||||
} else {
|
||||
this.item.next(person);
|
||||
this.connectionStatus.next(false);
|
||||
}
|
||||
|
||||
// update route removing the code from query params
|
||||
const redirectUrl = this.router.url.split('?')[0];
|
||||
this.router.navigate([redirectUrl]);
|
||||
});
|
||||
}
|
||||
}
|
31
src/app/item-page/orcid-page/orcid-page.guard.ts
Normal file
31
src/app/item-page/orcid-page/orcid-page.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ItemPageResolver } from '../item-page.resolver';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||
*/
|
||||
export class OrcidPageGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check administrator authorization rights
|
||||
*/
|
||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanSynchronizeWithORCID);
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
<div class="container mb-5">
|
||||
<h2>{{'person.orcid.sync.setting' | translate}}</h2>
|
||||
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md">
|
||||
<div class="card" data-test="sync-mode">
|
||||
<div class="card-header">{{ 'person.page.orcid.synchronization-mode'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-alert [type]="'alert-info'">
|
||||
{{ 'person.page.orcid.synchronization-mode-message' | translate}}
|
||||
</ds-alert>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="syncMode">{{ 'person.page.orcid.synchronization-mode.label'| translate }}</label>
|
||||
<select class="form-control" [(ngModel)]="currentSyncMode" name="syncMode" id="syncMode"
|
||||
required>
|
||||
<option *ngFor="let syncMode of syncModes"
|
||||
[value]="syncMode.value">{{ syncMode.label | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md mb-3">
|
||||
<div class="card h-100" data-test="sync-mode-publication">
|
||||
<div class="card-header">{{ 'person.page.orcid.publications-preferences'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-alert [type]="'alert-info'">
|
||||
{{ 'person.page.orcid.synchronization-mode-publication-message' | translate}}
|
||||
</ds-alert>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div *ngFor="let option of syncPublicationOptions" class="row form-check">
|
||||
<input type="radio" [(ngModel)]="currentSyncPublications"
|
||||
name="syncPublications" id="publicationOption_{{option.value}}" [value]="option.value"
|
||||
required>
|
||||
<label for="publicationOption_{{option.value}}"
|
||||
class="ml-2">{{option.label | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md mb-3">
|
||||
<div class="card h-100" data-test="sync-mode-funding">
|
||||
<div class="card-header">{{ 'person.page.orcid.funding-preferences'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-alert [type]="'alert-info'">
|
||||
{{ 'person.page.orcid.synchronization-mode-funding-message' | translate}}
|
||||
</ds-alert>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div *ngFor="let option of syncFundingOptions" class="row form-check">
|
||||
<input type="radio" [(ngModel)]="currentSyncFunding"
|
||||
name="syncFundings" id="fundingOption_{{option.value}}" [value]="option.value"
|
||||
required>
|
||||
<label for="fundingOption_{{option.value}}" class="ml-2">{{option.label | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md mb-3">
|
||||
<div class="card h-100" data-test="profile-preferences">
|
||||
<div class="card-header">{{ 'person.page.orcid.profile-preferences'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-alert [type]="'alert-info'">
|
||||
{{ 'person.page.orcid.synchronization-mode-profile-message' | translate}}
|
||||
</ds-alert>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div *ngFor="let option of syncProfileOptions" class="row form-check">
|
||||
<input type="checkbox" [(ngModel)]="option.checked"
|
||||
name="syncProfile_{{option.value}}" id="profileOption_{{option.value}}"
|
||||
[value]="option.value">
|
||||
<label for="profileOption_{{option.value}}" class="ml-2">{{option.label | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-primary float-right">
|
||||
<i class="fas fa-edit"></i>
|
||||
{{ 'person.page.orcid.save.preference.changes' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@@ -0,0 +1,261 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
|
||||
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { OrcidSyncSettingsComponent } from './orcid-sync-settings.component';
|
||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||
|
||||
describe('OrcidAuthComponent test suite', () => {
|
||||
let comp: OrcidSyncSettingsComponent;
|
||||
let fixture: ComponentFixture<OrcidSyncSettingsComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||
let notificationsService;
|
||||
let formGroup: FormGroup;
|
||||
|
||||
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||
id: 'test-id',
|
||||
visible: true,
|
||||
type: 'profile',
|
||||
_links: {
|
||||
item: {
|
||||
href: 'https://rest.api/rest/api/profiles/test-id/item'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/profiles/test-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
|
||||
}],
|
||||
'dspace.orcid.sync-mode': [{
|
||||
'value': 'MANUAL',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.sync-profile': [{
|
||||
'value': 'BIOGRAPHICAL',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}, {
|
||||
'value': 'IDENTIFIERS',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 1
|
||||
}],
|
||||
'dspace.orcid.sync-publications': [{
|
||||
'value': 'ALL',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}],
|
||||
'person.identifier.orcid': [{
|
||||
'value': 'orcid-id',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
||||
findByRelatedItem: jasmine.createSpy('findByRelatedItem'),
|
||||
updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations')
|
||||
});
|
||||
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
NgbAccordionModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [OrcidSyncSettingsComponent],
|
||||
providers: [
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
{ provide: ResearcherProfileService, useValue: researcherProfileService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(OrcidSyncSettingsComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
fixture = TestBed.createComponent(OrcidSyncSettingsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create cards properly', () => {
|
||||
const modes = fixture.debugElement.query(By.css('[data-test="sync-mode"]'));
|
||||
const publication = fixture.debugElement.query(By.css('[data-test="sync-mode-publication"]'));
|
||||
const funding = fixture.debugElement.query(By.css('[data-test="sync-mode-funding"]'));
|
||||
const preferences = fixture.debugElement.query(By.css('[data-test="profile-preferences"]'));
|
||||
expect(modes).toBeTruthy();
|
||||
expect(publication).toBeTruthy();
|
||||
expect(funding).toBeTruthy();
|
||||
expect(preferences).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should init sync modes properly', () => {
|
||||
expect(comp.currentSyncMode).toBe('MANUAL');
|
||||
expect(comp.currentSyncPublications).toBe('ALL');
|
||||
expect(comp.currentSyncFunding).toBe('DISABLED');
|
||||
});
|
||||
|
||||
describe('form submit', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
notificationsService = (comp as any).notificationsService;
|
||||
formGroup = new FormGroup({
|
||||
syncMode: new FormControl('MANUAL'),
|
||||
syncFundings: new FormControl('ALL'),
|
||||
syncPublications: new FormControl('ALL'),
|
||||
syncProfile_BIOGRAPHICAL: new FormControl(true),
|
||||
syncProfile_IDENTIFIERS: new FormControl(true),
|
||||
});
|
||||
spyOn(comp.settingsUpdated, 'emit');
|
||||
});
|
||||
|
||||
it('should call updateByOrcidOperations properly', () => {
|
||||
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
const expectedOps: Operation[] = [
|
||||
{
|
||||
path: '/orcid/mode',
|
||||
op: 'replace',
|
||||
value: 'MANUAL'
|
||||
}, {
|
||||
path: '/orcid/publications',
|
||||
op: 'replace',
|
||||
value: 'ALL'
|
||||
}, {
|
||||
path: '/orcid/fundings',
|
||||
op: 'replace',
|
||||
value: 'ALL'
|
||||
}, {
|
||||
path: '/orcid/profile',
|
||||
op: 'replace',
|
||||
value: 'BIOGRAPHICAL,IDENTIFIERS'
|
||||
}
|
||||
];
|
||||
|
||||
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||
scheduler.flush();
|
||||
|
||||
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(mockResearcherProfile, expectedOps);
|
||||
});
|
||||
|
||||
it('should show notification on success', () => {
|
||||
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
|
||||
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(comp.settingsUpdated.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show notification on error', () => {
|
||||
researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$());
|
||||
|
||||
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(comp.settingsUpdated.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show notification on error', () => {
|
||||
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
researcherProfileService.updateByOrcidOperations.and.returnValue(createFailedRemoteDataObject$());
|
||||
|
||||
scheduler.schedule(() => comp.onSubmit(formGroup));
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(comp.settingsUpdated.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,196 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-orcid-sync-setting',
|
||||
templateUrl: './orcid-sync-settings.component.html',
|
||||
styleUrls: ['./orcid-sync-settings.component.scss']
|
||||
})
|
||||
export class OrcidSyncSettingsComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The item for which showing the orcid settings
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The prefix used for i18n keys
|
||||
*/
|
||||
messagePrefix = 'person.page.orcid';
|
||||
|
||||
/**
|
||||
* The current synchronization mode
|
||||
*/
|
||||
currentSyncMode: string;
|
||||
|
||||
/**
|
||||
* The current synchronization mode for publications
|
||||
*/
|
||||
currentSyncPublications: string;
|
||||
|
||||
/**
|
||||
* The current synchronization mode for funding
|
||||
*/
|
||||
currentSyncFunding: string;
|
||||
|
||||
/**
|
||||
* The synchronization options
|
||||
*/
|
||||
syncModes: { value: string, label: string }[];
|
||||
|
||||
/**
|
||||
* The synchronization options for publications
|
||||
*/
|
||||
syncPublicationOptions: { value: string, label: string }[];
|
||||
|
||||
/**
|
||||
* The synchronization options for funding
|
||||
*/
|
||||
syncFundingOptions: { value: string, label: string }[];
|
||||
|
||||
/**
|
||||
* The profile synchronization options
|
||||
*/
|
||||
syncProfileOptions: { value: string, label: string, checked: boolean }[];
|
||||
|
||||
/**
|
||||
* An event emitted when settings are updated
|
||||
*/
|
||||
@Output() settingsUpdated: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(private researcherProfileService: ResearcherProfileService,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Init orcid settings form
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.syncModes = [
|
||||
{
|
||||
label: this.messagePrefix + '.synchronization-mode.batch',
|
||||
value: 'BATCH'
|
||||
},
|
||||
{
|
||||
label: this.messagePrefix + '.synchronization-mode.manual',
|
||||
value: 'MANUAL'
|
||||
}
|
||||
];
|
||||
|
||||
this.syncPublicationOptions = ['DISABLED', 'ALL']
|
||||
.map((value) => {
|
||||
return {
|
||||
label: this.messagePrefix + '.sync-publications.' + value.toLowerCase(),
|
||||
value: value,
|
||||
};
|
||||
});
|
||||
|
||||
this.syncFundingOptions = ['DISABLED', 'ALL']
|
||||
.map((value) => {
|
||||
return {
|
||||
label: this.messagePrefix + '.sync-fundings.' + value.toLowerCase(),
|
||||
value: value,
|
||||
};
|
||||
});
|
||||
|
||||
const syncProfilePreferences = this.item.allMetadataValues('dspace.orcid.sync-profile');
|
||||
|
||||
this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS']
|
||||
.map((value) => {
|
||||
return {
|
||||
label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(),
|
||||
value: value,
|
||||
checked: syncProfilePreferences.includes(value)
|
||||
};
|
||||
});
|
||||
|
||||
this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL');
|
||||
this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED');
|
||||
this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path operations to save orcid synchronization preferences
|
||||
*
|
||||
* @param form The form group
|
||||
*/
|
||||
onSubmit(form: FormGroup): void {
|
||||
const operations: Operation[] = [];
|
||||
this.fillOperationsFor(operations, '/orcid/mode', form.value.syncMode);
|
||||
this.fillOperationsFor(operations, '/orcid/publications', form.value.syncPublications);
|
||||
this.fillOperationsFor(operations, '/orcid/fundings', form.value.syncFundings);
|
||||
|
||||
const syncProfileValue = this.syncProfileOptions
|
||||
.map((syncProfileOption => syncProfileOption.value))
|
||||
.filter((value) => form.value['syncProfile_' + value])
|
||||
.join(',');
|
||||
|
||||
this.fillOperationsFor(operations, '/orcid/profile', syncProfileValue);
|
||||
|
||||
if (operations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.researcherProfileService.findByRelatedItem(this.item).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((profileRD: RemoteData<ResearcherProfile>) => {
|
||||
if (profileRD.hasSucceeded) {
|
||||
return this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
);
|
||||
} else {
|
||||
return of(profileRD);
|
||||
}
|
||||
}),
|
||||
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
||||
if (remoteData.isSuccess) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success'));
|
||||
this.settingsUpdated.emit();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve setting saved in the item's metadata
|
||||
*
|
||||
* @param metadataField The metadata name that contains setting
|
||||
* @param allowedValues The allowed values
|
||||
* @param defaultValue The default value
|
||||
* @private
|
||||
*/
|
||||
private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string {
|
||||
const currentPreference = this.item.firstMetadataValue(metadataField);
|
||||
return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a replace patch operation
|
||||
*
|
||||
* @param operations
|
||||
* @param path
|
||||
* @param currentValue
|
||||
*/
|
||||
private fillOperationsFor(operations: Operation[], path: string, currentValue: string): void {
|
||||
operations.push({
|
||||
path: path,
|
||||
op: 'replace',
|
||||
value: currentValue
|
||||
});
|
||||
}
|
||||
|
||||
}
|
10
src/app/page-error/page-error.component.html
Normal file
10
src/app/page-error/page-error.component.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="page-internal-server-error container">
|
||||
<h1 data-test="status">{{status}}</h1>
|
||||
<h2><small>{{"error-page.description." + status | translate}}</small></h2>
|
||||
<br/>
|
||||
<p>{{"error-page." + code | translate}}</p>
|
||||
<br/>
|
||||
<p class="text-center">
|
||||
<a href="/home" class="btn btn-primary">{{ status + ".link.home-page" | translate}}</a>
|
||||
</p>
|
||||
</div>
|
0
src/app/page-error/page-error.component.scss
Normal file
0
src/app/page-error/page-error.component.scss
Normal file
48
src/app/page-error/page-error.component.spec.ts
Normal file
48
src/app/page-error/page-error.component.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { PageErrorComponent } from './page-error.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
|
||||
|
||||
describe('PageErrorComponent', () => {
|
||||
let component: PageErrorComponent;
|
||||
let fixture: ComponentFixture<PageErrorComponent>;
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
queryParams: observableOf({
|
||||
status: 401,
|
||||
code: 'orcid.generic-error'
|
||||
})
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PageErrorComponent ],
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PageErrorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show error for 401 unauthorized', () => {
|
||||
const statusElement = fixture.debugElement.query(By.css('[data-test="status"]')).nativeElement;
|
||||
expect(statusElement.innerHTML).toEqual('401');
|
||||
});
|
||||
});
|
27
src/app/page-error/page-error.component.ts
Normal file
27
src/app/page-error/page-error.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* This component representing the `PageError` DSpace page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-page-error',
|
||||
styleUrls: ['./page-error.component.scss'],
|
||||
templateUrl: './page-error.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
export class PageErrorComponent {
|
||||
status: number;
|
||||
code: string;
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {ActivatedRoute} activatedRoute
|
||||
*/
|
||||
constructor(private activatedRoute: ActivatedRoute) {
|
||||
this.activatedRoute.queryParams.subscribe((params) => {
|
||||
this.status = params.status;
|
||||
this.code = params.code;
|
||||
});
|
||||
}
|
||||
}
|
26
src/app/page-error/themed-page-error.component.ts
Normal file
26
src/app/page-error/themed-page-error.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { PageErrorComponent } from './page-error.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for PageErrorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-search-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedPageErrorComponent extends ThemedComponent<PageErrorComponent> {
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'PageErrorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/page-error/page-error.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`src/app/page-error/page-error.component`);
|
||||
}
|
||||
}
|
@@ -40,6 +40,8 @@ import {
|
||||
import {
|
||||
PageInternalServerErrorComponent
|
||||
} from './page-internal-server-error/page-internal-server-error.component';
|
||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||
import { PageErrorComponent } from './page-error/page-error.component';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -74,7 +76,9 @@ const DECLARATIONS = [
|
||||
ThemedForbiddenComponent,
|
||||
IdleModalComponent,
|
||||
ThemedPageInternalServerErrorComponent,
|
||||
PageInternalServerErrorComponent
|
||||
PageInternalServerErrorComponent,
|
||||
ThemedPageErrorComponent,
|
||||
PageErrorComponent
|
||||
];
|
||||
|
||||
const EXPORTS = [
|
||||
|
@@ -1,3 +1 @@
|
||||
.btn-dark {
|
||||
background-color: var(--ds-admin-sidebar-bg);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<a *ngIf="isAuthorized | async"
|
||||
[ngbTooltip]="'item.page.orcid.tooltip' | translate"
|
||||
[routerLink]="[pageRoute, 'orcid']"
|
||||
class="btn btn-dark btn-sm"
|
||||
role="button" ><i class="fab fa-orcid fa-lg"></i>
|
||||
</a>
|
@@ -0,0 +1,76 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DsoPageOrcidButtonComponent } from './dso-page-orcid-button.component';
|
||||
|
||||
describe('DsoPageOrcidButtonComponent', () => {
|
||||
let component: DsoPageOrcidButtonComponent;
|
||||
let fixture: ComponentFixture<DsoPageOrcidButtonComponent>;
|
||||
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let dso: DSpaceObject;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
dso = Object.assign(new Item(), {
|
||||
id: 'test-item',
|
||||
_links: {
|
||||
self: { href: 'test-item-selflink' }
|
||||
}
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DsoPageOrcidButtonComponent],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule],
|
||||
providers: [
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService }
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DsoPageOrcidButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.dso = dso;
|
||||
component.pageRoute = 'test';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should check the authorization of the current user', () => {
|
||||
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanSynchronizeWithORCID, dso.self);
|
||||
});
|
||||
|
||||
describe('when the user is authorized', () => {
|
||||
beforeEach(() => {
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render a link', () => {
|
||||
const link = fixture.debugElement.query(By.css('a'));
|
||||
expect(link).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is not authorized', () => {
|
||||
beforeEach(() => {
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not render a link', () => {
|
||||
const link = fixture.debugElement.query(By.css('a'));
|
||||
expect(link).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,39 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-page-orcid-button',
|
||||
templateUrl: './dso-page-orcid-button.component.html',
|
||||
styleUrls: ['./dso-page-orcid-button.component.scss']
|
||||
})
|
||||
export class DsoPageOrcidButtonComponent implements OnInit {
|
||||
/**
|
||||
* The DSpaceObject to display a button to the edit page for
|
||||
*/
|
||||
@Input() dso: DSpaceObject;
|
||||
|
||||
/**
|
||||
* The prefix of the route to the edit page (before the object's UUID, e.g. "items")
|
||||
*/
|
||||
@Input() pageRoute: string;
|
||||
|
||||
/**
|
||||
* Whether or not the current user is authorized to edit the DSpaceObject
|
||||
*/
|
||||
isAuthorized: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
constructor(protected authorizationService: AuthorizationDataService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, this.dso.self).pipe(take(1)).subscribe((isAuthorized: boolean) => {
|
||||
this.isAuthorized.next(isAuthorized);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -1 +1,7 @@
|
||||
<button class="edit-button btn btn-dark btn-sm" data-test="item-claim" *ngIf="(isClaimable() | async)" (click)="claim()"> {{"item.page.claim.button" | translate }} </button>
|
||||
<button *ngIf="(isClaimable() | async)"
|
||||
[ngbTooltip]="'item.page.claim.tooltip' | translate"
|
||||
class="edit-button btn btn-dark btn-sm"
|
||||
data-test="item-claim"
|
||||
(click)="claim()">
|
||||
{{'item.page.claim.button' | translate }}
|
||||
</button>
|
||||
|
@@ -0,0 +1,110 @@
|
||||
import { Component, Inject, OnInit, } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||
import { isEmpty, isNotNull } from '../../empty.util';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { CoreState } from '../../../core/core-state.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-external-provider',
|
||||
template: ''
|
||||
|
||||
})
|
||||
export abstract class LogInExternalProviderComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* True if the authentication is loading.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The shibboleth authentication location url.
|
||||
* @type {string}
|
||||
*/
|
||||
public location: string;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {boolean} isStandalonePage
|
||||
* @param {NativeWindowRef} _window
|
||||
* @param {AuthService} authService
|
||||
* @param {HardRedirectService} hardRedirectService
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private authService: AuthService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set location
|
||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the external provider url for login
|
||||
*/
|
||||
redirectToExternalProvider() {
|
||||
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
|
||||
if (!this.isStandalonePage) {
|
||||
redirectRoute = this.hardRedirectService.getCurrentRoute();
|
||||
} else if (isEmpty(redirectRoute)) {
|
||||
redirectRoute = '/';
|
||||
}
|
||||
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
|
||||
|
||||
let externalServerUrl = this.location;
|
||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||
const match = myRegexp.exec(this.location);
|
||||
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
|
||||
|
||||
// Check whether the current page is different from the redirect url received from rest
|
||||
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
|
||||
// change the redirect url with the current page url
|
||||
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
|
||||
externalServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
||||
}
|
||||
|
||||
// redirect to shibboleth authentication url
|
||||
this.hardRedirectService.redirect(externalServerUrl);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -1,110 +1,21 @@
|
||||
import { Component, Inject, OnInit, } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Component, } from '@angular/core';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { isNotNull, isEmpty } from '../../../empty.util';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
|
||||
import { CoreState } from '../../../../core/core-state.model';
|
||||
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-oidc',
|
||||
templateUrl: './log-in-oidc.component.html',
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Oidc)
|
||||
export class LogInOidcComponent implements OnInit {
|
||||
export class LogInOidcComponent extends LogInExternalProviderComponent {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
* Redirect to orcid authentication url
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* True if the authentication is loading.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The oidc authentication location url.
|
||||
* @type {string}
|
||||
*/
|
||||
public location: string;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {boolean} isStandalonePage
|
||||
* @param {NativeWindowRef} _window
|
||||
* @param {AuthService} authService
|
||||
* @param {HardRedirectService} hardRedirectService
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private authService: AuthService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set location
|
||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
||||
|
||||
}
|
||||
|
||||
redirectToOidc() {
|
||||
|
||||
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
|
||||
if (!this.isStandalonePage) {
|
||||
redirectRoute = this.hardRedirectService.getCurrentRoute();
|
||||
} else if (isEmpty(redirectRoute)) {
|
||||
redirectRoute = '/';
|
||||
}
|
||||
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
|
||||
|
||||
let oidcServerUrl = this.location;
|
||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||
const match = myRegexp.exec(this.location);
|
||||
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
|
||||
|
||||
// Check whether the current page is different from the redirect url received from rest
|
||||
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
|
||||
// change the redirect url with the current page url
|
||||
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
|
||||
oidcServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
||||
}
|
||||
|
||||
// redirect to oidc authentication url
|
||||
this.hardRedirectService.redirect(oidcServerUrl);
|
||||
});
|
||||
|
||||
this.redirectToExternalProvider();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToOrcid()">
|
||||
<i class="fas fa-sign-in-alt"></i> {{"login.form.orcid" | translate}}
|
||||
</button>
|
@@ -0,0 +1,155 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { EPersonMock } from '../../../testing/eperson.mock';
|
||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||
import { storeModuleConfig } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInOrcidComponent } from './log-in-orcid.component';
|
||||
import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
|
||||
describe('LogInOrcidComponent', () => {
|
||||
|
||||
let component: LogInOrcidComponent;
|
||||
let fixture: ComponentFixture<LogInOrcidComponent>;
|
||||
let page: Page;
|
||||
let user: EPerson;
|
||||
let componentAsAny: any;
|
||||
let setHrefSpy;
|
||||
let orcidBaseUrl;
|
||||
let location;
|
||||
let initialState: any;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
orcidBaseUrl = 'dspace-rest.test/orcid?redirectUrl=';
|
||||
location = orcidBaseUrl + 'http://dspace-angular.test/home';
|
||||
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
getCurrentRoute: {},
|
||||
redirect: {}
|
||||
});
|
||||
|
||||
initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
blocking: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
LogInOrcidComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, location) },
|
||||
{ provide: 'isStandalonePage', useValue: true },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
provideMockStore({ initialState }),
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// create component and test fixture
|
||||
fixture = TestBed.createComponent(LogInOrcidComponent);
|
||||
|
||||
// get test component from the fixture
|
||||
component = fixture.componentInstance;
|
||||
componentAsAny = component;
|
||||
|
||||
// create page
|
||||
page = new Page(component, fixture);
|
||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||
|
||||
});
|
||||
|
||||
it('should set the properly a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/collections/12345';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToOrcid();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
});
|
||||
|
||||
it('should not set a new redirectUrl', () => {
|
||||
const currentUrl = 'http://dspace-angular.test/home';
|
||||
componentAsAny._window.nativeWindow.location.href = currentUrl;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(componentAsAny.injectedAuthMethodModel.location).toBe(location);
|
||||
expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl);
|
||||
|
||||
component.redirectToOrcid();
|
||||
|
||||
expect(setHrefSpy).toHaveBeenCalledWith(currentUrl);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* I represent the DOM elements and attach spies.
|
||||
*
|
||||
* @class Page
|
||||
*/
|
||||
class Page {
|
||||
|
||||
public emailInput: HTMLInputElement;
|
||||
public navigateSpy: jasmine.Spy;
|
||||
public passwordInput: HTMLInputElement;
|
||||
|
||||
constructor(private component: LogInOrcidComponent, private fixture: ComponentFixture<LogInOrcidComponent>) {
|
||||
// use injector to get services
|
||||
const injector = fixture.debugElement.injector;
|
||||
const store = injector.get(Store);
|
||||
|
||||
// add spies
|
||||
this.navigateSpy = spyOn(store, 'dispatch');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { Component, } from '@angular/core';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-orcid',
|
||||
templateUrl: './log-in-orcid.component.html',
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Orcid)
|
||||
export class LogInOrcidComponent extends LogInExternalProviderComponent {
|
||||
|
||||
/**
|
||||
* Redirect to orcid authentication url
|
||||
*/
|
||||
redirectToOrcid() {
|
||||
this.redirectToExternalProvider();
|
||||
}
|
||||
|
||||
}
|
@@ -1,21 +1,8 @@
|
||||
import { Component, Inject, OnInit, } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Component, } from '@angular/core';
|
||||
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
|
||||
import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { isNotNull, isEmpty } from '../../../empty.util';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { URLCombiner } from '../../../../core/url-combiner/url-combiner';
|
||||
import { CoreState } from '../../../../core/core-state.model';
|
||||
import { LogInExternalProviderComponent } from '../log-in-external-provider.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-shibboleth',
|
||||
@@ -24,92 +11,13 @@ import { CoreState } from '../../../../core/core-state.model';
|
||||
|
||||
})
|
||||
@renderAuthMethodFor(AuthMethodType.Shibboleth)
|
||||
export class LogInShibbolethComponent implements OnInit {
|
||||
export class LogInShibbolethComponent extends LogInExternalProviderComponent {
|
||||
|
||||
/**
|
||||
* The authentication method data.
|
||||
* @type {AuthMethod}
|
||||
* Redirect to shibboleth authentication url
|
||||
*/
|
||||
public authMethod: AuthMethod;
|
||||
|
||||
/**
|
||||
* True if the authentication is loading.
|
||||
* @type {boolean}
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The shibboleth authentication location url.
|
||||
* @type {string}
|
||||
*/
|
||||
public location: string;
|
||||
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {boolean} isStandalonePage
|
||||
* @param {NativeWindowRef} _window
|
||||
* @param {RouteService} route
|
||||
* @param {AuthService} authService
|
||||
* @param {HardRedirectService} hardRedirectService
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private route: RouteService,
|
||||
private authService: AuthService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// set loading
|
||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||
|
||||
// set location
|
||||
this.location = decodeURIComponent(this.injectedAuthMethodModel.location);
|
||||
|
||||
}
|
||||
|
||||
redirectToShibboleth() {
|
||||
|
||||
this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => {
|
||||
if (!this.isStandalonePage) {
|
||||
redirectRoute = this.hardRedirectService.getCurrentRoute();
|
||||
} else if (isEmpty(redirectRoute)) {
|
||||
redirectRoute = '/';
|
||||
}
|
||||
const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString();
|
||||
|
||||
let shibbolethServerUrl = this.location;
|
||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||
const match = myRegexp.exec(this.location);
|
||||
const redirectUrlFromServer = (match && match[1]) ? match[1] : null;
|
||||
|
||||
// Check whether the current page is different from the redirect url received from rest
|
||||
if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) {
|
||||
// change the redirect url with the current page url
|
||||
const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`;
|
||||
shibbolethServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl);
|
||||
}
|
||||
|
||||
// redirect to shibboleth authentication url
|
||||
this.hardRedirectService.redirect(shibbolethServerUrl);
|
||||
});
|
||||
|
||||
this.redirectToExternalProvider();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -29,4 +29,8 @@ export class RouterMock {
|
||||
createUrlTree(commands, navExtras = {}) {
|
||||
return {};
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.routerState.snapshot.url;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="alert {{notification.type}} alert-dismissible m-3 shadow" role="alert"
|
||||
<div class="notification alert {{notification.type}} alert-dismissible m-3 shadow" role="alert"
|
||||
[@enterLeave]="animate">
|
||||
|
||||
<div class="notification-progress-loader position-absolute w-100" *ngIf="showProgressBar">
|
||||
|
@@ -1,4 +1,4 @@
|
||||
.alert {
|
||||
.notification {
|
||||
display: inline-block;
|
||||
min-width: var(--bs-modal-sm);
|
||||
text-align: left;
|
||||
|
@@ -0,0 +1,7 @@
|
||||
<button *ngIf="shouldShowButton$ | async"
|
||||
class="export-button btn btn-dark btn-sm"
|
||||
[ngbTooltip]="tooltipMsg | translate"
|
||||
(click)="export()"
|
||||
[title]="tooltipMsg |translate" [attr.aria-label]="tooltipMsg |translate">
|
||||
<i class="fas fa-file-export fa-fw"></i>
|
||||
</button>
|
@@ -0,0 +1,4 @@
|
||||
.export-button {
|
||||
background: var(--ds-admin-sidebar-bg);
|
||||
border-color: var(--ds-admin-sidebar-bg);
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { SearchExportCsvComponent } from './search-export-csv.component';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||
import { Script } from '../../../process-page/scripts/script.model';
|
||||
import { Process } from '../../../process-page/processes/process.model';
|
||||
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { getProcessDetailRoute } from '../../../process-page/process-page-routing.paths';
|
||||
import { SearchFilter } from '../models/search-filter.model';
|
||||
import { PaginatedSearchOptions } from '../models/paginated-search-options.model';
|
||||
|
||||
describe('SearchExportCsvComponent', () => {
|
||||
let component: SearchExportCsvComponent;
|
||||
let fixture: ComponentFixture<SearchExportCsvComponent>;
|
||||
|
||||
let scriptDataService: ScriptDataService;
|
||||
let authorizationDataService: AuthorizationDataService;
|
||||
let notificationsService;
|
||||
let router;
|
||||
|
||||
const script = Object.assign(new Script(), {id: 'metadata-export-search', name: 'metadata-export-search'});
|
||||
const process = Object.assign(new Process(), {processId: 5, scriptName: 'metadata-export-search'});
|
||||
|
||||
const searchConfig = new PaginatedSearchOptions({
|
||||
configuration: 'test-configuration',
|
||||
scope: 'test-scope',
|
||||
query: 'test-query',
|
||||
filters: [
|
||||
new SearchFilter('f.filter1', ['filter1value1,equals', 'filter1value2,equals']),
|
||||
new SearchFilter('f.filter2', ['filter2value1,contains']),
|
||||
new SearchFilter('f.filter3', ['[2000 TO 2001]'], 'equals')
|
||||
]
|
||||
});
|
||||
|
||||
function initBeforeEachAsync() {
|
||||
scriptDataService = jasmine.createSpyObj('scriptDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(script),
|
||||
invoke: createSuccessfulRemoteDataObject$(process)
|
||||
});
|
||||
authorizationDataService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
router = jasmine.createSpyObj('authorizationService', ['navigateByUrl']);
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SearchExportCsvComponent],
|
||||
imports: [TranslateModule.forRoot(), NgbModule],
|
||||
providers: [
|
||||
{provide: ScriptDataService, useValue: scriptDataService},
|
||||
{provide: AuthorizationDataService, useValue: authorizationDataService},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: Router, useValue: router},
|
||||
]
|
||||
}).compileComponents();
|
||||
}
|
||||
|
||||
function initBeforeEach() {
|
||||
fixture = TestBed.createComponent(SearchExportCsvComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.searchConfig = searchConfig;
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
describe('comp', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
initBeforeEachAsync();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
initBeforeEach();
|
||||
});
|
||||
it('should init the comp', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('when the user is an admin and the metadata-export-search script is present ', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
initBeforeEachAsync();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
initBeforeEach();
|
||||
});
|
||||
it('should add the button', () => {
|
||||
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
|
||||
expect(debugElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe('when the user is not an admin', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
initBeforeEachAsync();
|
||||
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
}));
|
||||
beforeEach(() => {
|
||||
initBeforeEach();
|
||||
});
|
||||
it('should not add the button', () => {
|
||||
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
|
||||
expect(debugElement).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('when the metadata-export-search script is not present', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
initBeforeEachAsync();
|
||||
(scriptDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Not found', 404));
|
||||
}));
|
||||
beforeEach(() => {
|
||||
initBeforeEach();
|
||||
});
|
||||
it('should should not add the button', () => {
|
||||
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
|
||||
expect(debugElement).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('export', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
initBeforeEachAsync();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
initBeforeEach();
|
||||
});
|
||||
it('should call the invoke script method with the correct parameters', () => {
|
||||
component.export();
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-search',
|
||||
[
|
||||
{name: '-q', value: searchConfig.query},
|
||||
{name: '-s', value: searchConfig.scope},
|
||||
{name: '-c', value: searchConfig.configuration},
|
||||
{name: '-f', value: 'filter1,equals=filter1value1'},
|
||||
{name: '-f', value: 'filter1,equals=filter1value2'},
|
||||
{name: '-f', value: 'filter2,contains=filter2value1'},
|
||||
{name: '-f', value: 'filter3,equals=[2000 TO 2001]'},
|
||||
], []);
|
||||
|
||||
component.searchConfig = null;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.export();
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-search', [], []);
|
||||
|
||||
});
|
||||
it('should show a success message when the script was invoked successfully and redirect to the corresponding process page', () => {
|
||||
component.export();
|
||||
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessDetailRoute(process.processId));
|
||||
});
|
||||
it('should show an error message when the script was not invoked successfully and stay on the current page', () => {
|
||||
(scriptDataService.invoke as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||
|
||||
component.export();
|
||||
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('clicking the button', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
initBeforeEachAsync();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
initBeforeEach();
|
||||
});
|
||||
it('should trigger the export function', () => {
|
||||
spyOn(component, 'export');
|
||||
|
||||
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
|
||||
debugElement.triggerEventHandler('click', null);
|
||||
|
||||
expect(component.export).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,110 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { hasValue, isNotEmpty } from '../../empty.util';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Process } from '../../../process-page/processes/process.model';
|
||||
import { getProcessDetailRoute } from '../../../process-page/process-page-routing.paths';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { PaginatedSearchOptions } from '../models/paginated-search-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-export-csv',
|
||||
styleUrls: ['./search-export-csv.component.scss'],
|
||||
templateUrl: './search-export-csv.component.html',
|
||||
})
|
||||
/**
|
||||
* Display a button to export the current search results as csv
|
||||
*/
|
||||
export class SearchExportCsvComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The current configuration of the search
|
||||
*/
|
||||
@Input() searchConfig: PaginatedSearchOptions;
|
||||
|
||||
/**
|
||||
* Observable used to determine whether the button should be shown
|
||||
*/
|
||||
shouldShowButton$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The message key used for the tooltip of the button
|
||||
*/
|
||||
tooltipMsg = 'metadata-export-search.tooltip';
|
||||
|
||||
constructor(private scriptDataService: ScriptDataService,
|
||||
private authorizationDataService: AuthorizationDataService,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const scriptExists$ = this.scriptDataService.findById('metadata-export-search').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd) => rd.isSuccess && hasValue(rd.payload))
|
||||
);
|
||||
|
||||
const isAuthorized$ = this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf);
|
||||
|
||||
this.shouldShowButton$ = observableCombineLatest([scriptExists$, isAuthorized$]).pipe(
|
||||
map(([scriptExists, isAuthorized]: [boolean, boolean]) => scriptExists && isAuthorized)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the export of the items based on the current search configuration
|
||||
*/
|
||||
export() {
|
||||
const parameters = [];
|
||||
if (hasValue(this.searchConfig)) {
|
||||
if (isNotEmpty(this.searchConfig.query)) {
|
||||
parameters.push({name: '-q', value: this.searchConfig.query});
|
||||
}
|
||||
if (isNotEmpty(this.searchConfig.scope)) {
|
||||
parameters.push({name: '-s', value: this.searchConfig.scope});
|
||||
}
|
||||
if (isNotEmpty(this.searchConfig.configuration)) {
|
||||
parameters.push({name: '-c', value: this.searchConfig.configuration});
|
||||
}
|
||||
if (isNotEmpty(this.searchConfig.filters)) {
|
||||
this.searchConfig.filters.forEach((filter) => {
|
||||
if (hasValue(filter.values)) {
|
||||
filter.values.forEach((value) => {
|
||||
let operator;
|
||||
let filterValue;
|
||||
if (hasValue(filter.operator)) {
|
||||
operator = filter.operator;
|
||||
filterValue = value;
|
||||
} else {
|
||||
operator = value.substring(value.lastIndexOf(',') + 1);
|
||||
filterValue = value.substring(0, value.lastIndexOf(','));
|
||||
}
|
||||
const valueToAdd = `${filter.key.substring(2)},${operator}=${filterValue}`;
|
||||
parameters.push({name: '-f', value: valueToAdd});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.scriptDataService.invoke('metadata-export-search', parameters, []).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd: RemoteData<Process>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('metadata-export-search.submit.success'));
|
||||
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('metadata-export-search.submit.error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,4 +1,7 @@
|
||||
<div class="d-flex justify-content-between">
|
||||
<h2 *ngIf="!disableHeader">{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}</h2>
|
||||
<ds-search-export-csv [searchConfig]="searchConfig"></ds-search-export-csv>
|
||||
</div>
|
||||
<div *ngIf="searchResults && searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
||||
<ds-viewable-collection
|
||||
[config]="searchConfig.pagination"
|
||||
|
@@ -286,8 +286,11 @@ import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'
|
||||
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
|
||||
import { RSSComponent } from './rss-feed/rss.component';
|
||||
import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component';
|
||||
import { DsoPageOrcidButtonComponent } from './dso-page/dso-page-orcid-button/dso-page-orcid-button.component';
|
||||
import { LogInOrcidComponent } from './log-in/methods/orcid/log-in-orcid.component';
|
||||
import { BrowserOnlyPipe } from './utils/browser-only.pipe';
|
||||
import { PersonPageClaimButtonComponent } from './dso-page/person-page-claim-button/person-page-claim-button.component';
|
||||
import { SearchExportCsvComponent } from './search/search-export-csv/search-export-csv.component';
|
||||
|
||||
const MODULES = [
|
||||
CommonModule,
|
||||
@@ -399,6 +402,7 @@ const COMPONENTS = [
|
||||
|
||||
CollectionSearchResultGridElementComponent,
|
||||
CommunitySearchResultGridElementComponent,
|
||||
SearchExportCsvComponent,
|
||||
PageSizeSelectorComponent,
|
||||
ListableObjectComponentLoaderComponent,
|
||||
CollectionListElementComponent,
|
||||
@@ -422,6 +426,7 @@ const COMPONENTS = [
|
||||
|
||||
LogInShibbolethComponent,
|
||||
LogInOidcComponent,
|
||||
LogInOrcidComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInContainerComponent,
|
||||
ItemVersionsComponent,
|
||||
@@ -494,6 +499,7 @@ const ENTRY_COMPONENTS = [
|
||||
LogInPasswordComponent,
|
||||
LogInShibbolethComponent,
|
||||
LogInOidcComponent,
|
||||
LogInOrcidComponent,
|
||||
BundleListElementComponent,
|
||||
ClaimedTaskActionsApproveComponent,
|
||||
ClaimedTaskActionsRejectComponent,
|
||||
@@ -528,6 +534,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
GenericItemPageFieldComponent,
|
||||
MetadataRepresentationListComponent,
|
||||
RelatedItemsComponent,
|
||||
DsoPageOrcidButtonComponent
|
||||
|
||||
];
|
||||
|
||||
|
@@ -3,7 +3,11 @@
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<button class="btn btn-link p-0 expandButton" dsDragClick (actualClick)="toggle()">
|
||||
<i class="fas fa-angle-down"></i> {{ 'item.truncatable-part.show-more' | translate }}</button>
|
||||
<i class="fas fa-angle-down"></i>
|
||||
<span class="ml-1">{{ 'item.truncatable-part.show-more' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-link p-0 collapseButton" dsDragClick (actualClick)="toggle()" *ngIf="expand && expandable">
|
||||
<i class="fas fa-angle-up"></i> {{ 'item.truncatable-part.show-less' | translate }}</button>
|
||||
<i class="fas fa-angle-up"></i>
|
||||
<span class="ml-1">{{ 'item.truncatable-part.show-less' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -27,6 +27,16 @@
|
||||
|
||||
"404.page-not-found": "page not found",
|
||||
|
||||
"error-page.description.401": "unauthorized",
|
||||
|
||||
"error-page.description.403": "forbidden",
|
||||
|
||||
"error-page.description.500": "Service Unavailable",
|
||||
|
||||
"error-page.description.404": "page not found",
|
||||
|
||||
"error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator",
|
||||
|
||||
"access-status.embargo.listelement.badge": "Embargo",
|
||||
|
||||
"access-status.metadata.only.listelement.badge": "Metadata only",
|
||||
@@ -2125,6 +2135,7 @@
|
||||
|
||||
"item.edit.withdraw.success": "The item was withdrawn successfully",
|
||||
|
||||
"item.orcid.return": "Back",
|
||||
|
||||
|
||||
"item.listelement.badge": "Item",
|
||||
@@ -2185,6 +2196,10 @@
|
||||
|
||||
"item.page.link.simple": "Simple item page",
|
||||
|
||||
"item.page.orcid.title": "ORCID",
|
||||
|
||||
"item.page.orcid.tooltip": "Open ORCID setting page",
|
||||
|
||||
"item.page.person.search.title": "Articles by this author",
|
||||
|
||||
"item.page.related-items.view-more": "Show {{ amount }} more",
|
||||
@@ -2219,6 +2234,8 @@
|
||||
|
||||
"item.page.claim.button": "Claim",
|
||||
|
||||
"item.page.claim.tooltip": "Claim this item as profile",
|
||||
|
||||
"item.preview.dc.identifier.uri": "Identifier:",
|
||||
|
||||
"item.preview.dc.contributor.author": "Authors:",
|
||||
@@ -2506,6 +2523,8 @@
|
||||
|
||||
"login.form.oidc": "Log in with OIDC",
|
||||
|
||||
"login.form.orcid": "Log in with ORCID",
|
||||
|
||||
"login.form.password": "Password",
|
||||
|
||||
"login.form.shibboleth": "Log in with Shibboleth",
|
||||
@@ -2704,6 +2723,11 @@
|
||||
"menu.section.workflow": "Administer Workflow",
|
||||
|
||||
|
||||
"metadata-export-search.tooltip": "Export search results as CSV",
|
||||
"metadata-export-search.submit.success": "The export was started successfully",
|
||||
"metadata-export-search.submit.error": "Starting the export has failed",
|
||||
|
||||
|
||||
"mydspace.breadcrumbs": "MyDSpace",
|
||||
|
||||
"mydspace.description": "",
|
||||
@@ -4485,4 +4509,205 @@
|
||||
"researcherprofile.success.claim.body" : "Profile claimed with success",
|
||||
|
||||
"researcherprofile.success.claim.title" : "Success",
|
||||
|
||||
"person.page.orcid": "ORCID",
|
||||
|
||||
"person.page.orcid.create": "Create an ORCID ID",
|
||||
|
||||
"person.page.orcid.granted-authorizations": "Granted authorizations",
|
||||
|
||||
"person.page.orcid.grant-authorizations" : "Grant authorizations",
|
||||
|
||||
"person.page.orcid.link": "Connect to ORCID ID",
|
||||
|
||||
"person.page.orcid.link.processing": "Linking profile to ORCID...",
|
||||
|
||||
"person.page.orcid.link.error.message": "Something went wrong while connecting the profile with ORCID. If the problem persists, contact the administrator.",
|
||||
|
||||
"person.page.orcid.orcid-not-linked-message": "The ORCID iD of this profile ({{ orcid }}) has not yet been connected to an account on the ORCID registry or the connection is expired.",
|
||||
|
||||
"person.page.orcid.unlink": "Disconnect from ORCID",
|
||||
|
||||
"person.page.orcid.unlink.processing": "Processing...",
|
||||
|
||||
"person.page.orcid.missing-authorizations": "Missing authorizations",
|
||||
|
||||
"person.page.orcid.missing-authorizations-message": "The following authorizations are missing:",
|
||||
|
||||
"person.page.orcid.no-missing-authorizations-message": "Great! This box is empty, so you have granted all access rights to use all functions offers by your institution.",
|
||||
|
||||
"person.page.orcid.no-orcid-message": "No ORCID iD associated yet. By clicking on the button below it is possible to link this profile with an ORCID account.",
|
||||
|
||||
"person.page.orcid.profile-preferences": "Profile preferences",
|
||||
|
||||
"person.page.orcid.funding-preferences": "Funding preferences",
|
||||
|
||||
"person.page.orcid.publications-preferences": "Publication preferences",
|
||||
|
||||
"person.page.orcid.remove-orcid-message": "If you need to remove your ORCID, please contact the repository administrator",
|
||||
|
||||
"person.page.orcid.save.preference.changes": "Update settings",
|
||||
|
||||
"person.page.orcid.sync-profile.affiliation" : "Affiliation",
|
||||
|
||||
"person.page.orcid.sync-profile.biographical" : "Biographical data",
|
||||
|
||||
"person.page.orcid.sync-profile.education" : "Education",
|
||||
|
||||
"person.page.orcid.sync-profile.identifiers" : "Identifiers",
|
||||
|
||||
"person.page.orcid.sync-fundings.all" : "All fundings",
|
||||
|
||||
"person.page.orcid.sync-fundings.mine" : "My fundings",
|
||||
|
||||
"person.page.orcid.sync-fundings.my_selected" : "Selected fundings",
|
||||
|
||||
"person.page.orcid.sync-fundings.disabled" : "Disabled",
|
||||
|
||||
"person.page.orcid.sync-publications.all" : "All publications",
|
||||
|
||||
"person.page.orcid.sync-publications.mine" : "My publications",
|
||||
|
||||
"person.page.orcid.sync-publications.my_selected" : "Selected publications",
|
||||
|
||||
"person.page.orcid.sync-publications.disabled" : "Disabled",
|
||||
|
||||
"person.page.orcid.sync-queue.discard" : "Discard the change and do not synchronize with the ORCID registry",
|
||||
|
||||
"person.page.orcid.sync-queue.discard.error": "The discarding of the ORCID queue record failed",
|
||||
|
||||
"person.page.orcid.sync-queue.discard.success": "The ORCID queue record have been discarded successfully",
|
||||
|
||||
"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.description.affiliation": "Affiliations",
|
||||
|
||||
"person.page.orcid.sync-queue.description.country": "Country",
|
||||
|
||||
"person.page.orcid.sync-queue.description.education": "Educations",
|
||||
|
||||
"person.page.orcid.sync-queue.description.external_ids": "External ids",
|
||||
|
||||
"person.page.orcid.sync-queue.description.other_names": "Other names",
|
||||
|
||||
"person.page.orcid.sync-queue.description.qualification": "Qualifications",
|
||||
|
||||
"person.page.orcid.sync-queue.description.researcher_urls": "Researcher urls",
|
||||
|
||||
"person.page.orcid.sync-queue.description.keywords": "Keywords",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.insert": "Add a new entry in the ORCID registry",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.update": "Update this entry on the ORCID registry",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.delete": "Remove this entry from the ORCID registry",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.publication": "Publication",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.funding": "Funding",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.affiliation": "Affiliation",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.education": "Education",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.qualification": "Qualification",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.other_names": "Other name",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.country": "Country",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.keywords": "Keyword",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.external_ids": "External identifier",
|
||||
|
||||
"person.page.orcid.sync-queue.tooltip.researcher_urls": "Researcher url",
|
||||
|
||||
"person.page.orcid.sync-queue.send" : "Synchronize with ORCID registry",
|
||||
|
||||
"person.page.orcid.sync-queue.send.unauthorized-error.title": "The submission to ORCID failed for missing authorizations.",
|
||||
|
||||
"person.page.orcid.sync-queue.send.unauthorized-error.content": "Click <a href='{{orcid}}'>here</a> to grant again the required permissions. If the problem persists, contact the administrator",
|
||||
|
||||
"person.page.orcid.sync-queue.send.bad-request-error": "The submission to ORCID failed because the resource sent to ORCID registry is not valid",
|
||||
|
||||
"person.page.orcid.sync-queue.send.error": "The submission to ORCID failed",
|
||||
|
||||
"person.page.orcid.sync-queue.send.conflict-error": "The submission to ORCID failed because the resource is already present on the ORCID registry",
|
||||
|
||||
"person.page.orcid.sync-queue.send.not-found-warning": "The resource does not exists anymore on the ORCID registry.",
|
||||
|
||||
"person.page.orcid.sync-queue.send.success": "The submission to ORCID was completed successfully",
|
||||
|
||||
"person.page.orcid.sync-queue.send.validation-error": "The data that you want to synchronize with ORCID is not valid",
|
||||
|
||||
"person.page.orcid.sync-queue.send.validation-error.amount-currency.required": "The amount's currency is required",
|
||||
|
||||
"person.page.orcid.sync-queue.send.validation-error.external-id.required": "The resource to be sent requires at least one identifier",
|
||||
|
||||
"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.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.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.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.country-required": "The address of the organization to be sent requires a 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.value-required": "The organization's identifiers requires a value",
|
||||
|
||||
"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.required": "The organization's identifiers requires a source",
|
||||
|
||||
"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "The source of one of the organization identifiers is invalid. Supported sources are RINGGOLD, GRID, LEI and FUNDREF",
|
||||
|
||||
"person.page.orcid.synchronization-mode": "Synchronization mode",
|
||||
|
||||
"person.page.orcid.synchronization-mode.batch": "Batch",
|
||||
|
||||
"person.page.orcid.synchronization-mode.label": "Synchronization mode",
|
||||
|
||||
"person.page.orcid.synchronization-mode-message": "Please select how you would like synchronization to ORCID to occur. The options include \"Manual\" (you must send your data to ORCID manually), or \"Batch\" (the system will send your data to ORCID via a scheduled script).",
|
||||
|
||||
"person.page.orcid.synchronization-mode-funding-message": "Select whether to send your linked Project entities to your ORCID record's list of funding information.",
|
||||
|
||||
"person.page.orcid.synchronization-mode-publication-message": "Select whether to send your linked Publication entities to your ORCID record's list of works.",
|
||||
|
||||
"person.page.orcid.synchronization-mode-profile-message": "Select whether to send your biographical data or personal identifiers to your ORCID record.",
|
||||
|
||||
"person.page.orcid.synchronization-settings-update.success": "The synchronization settings have been updated successfully",
|
||||
|
||||
"person.page.orcid.synchronization-settings-update.error": "The update of the synchronization settings failed",
|
||||
|
||||
"person.page.orcid.synchronization-mode.manual": "Manual",
|
||||
|
||||
"person.page.orcid.scope.authenticate": "Get your ORCID iD",
|
||||
|
||||
"person.page.orcid.scope.read-limited": "Read your information with visibility set to Trusted Parties",
|
||||
|
||||
"person.page.orcid.scope.activities-update": "Add/update your research activities",
|
||||
|
||||
"person.page.orcid.scope.person-update": "Add/update other information about you",
|
||||
|
||||
"person.page.orcid.unlink.success": "The disconnection between the profile and the ORCID registry was successful",
|
||||
|
||||
"person.page.orcid.unlink.error": "An error occurred while disconnecting between the profile and the ORCID registry. Try again",
|
||||
|
||||
"person.orcid.sync.setting": "ORCID Synchronization settings",
|
||||
|
||||
"person.orcid.registry.queue": "ORCID Registry Queue",
|
||||
|
||||
"person.orcid.registry.auth": "ORCID Authorizations",
|
||||
|
||||
}
|
||||
|
@@ -107,7 +107,7 @@
|
||||
"admin.registries.bitstream-formats.edit.head": "Tiedostoformaatti: {{ format }}",
|
||||
|
||||
// "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.",
|
||||
"admin.registries.bitstream-formats.edit.internal.hint": "Sisäisiksi merkittyjä formaatteja käytetään hallinnollisiin tarkoituksiin, ja ne on piilotettu käyttäjiltä.",
|
||||
"admin.registries.bitstream-formats.edit.internal.hint": "Sisäisiksi merkittyjä formaatteja käytetään ylläpitotarkoituksiin, ja ne on piilotettu käyttäjiltä.",
|
||||
|
||||
// "admin.registries.bitstream-formats.edit.internal.label": "Internal",
|
||||
"admin.registries.bitstream-formats.edit.internal.label": "Sisäinen",
|
||||
@@ -662,7 +662,7 @@
|
||||
|
||||
|
||||
// "admin.search.breadcrumbs": "Administrative Search",
|
||||
"admin.search.breadcrumbs": "Hallinnollinen haku",
|
||||
"admin.search.breadcrumbs": "Ylläpitäjän haku",
|
||||
|
||||
// "admin.search.collection.edit": "Edit",
|
||||
"admin.search.collection.edit": "Muokkaa",
|
||||
@@ -692,19 +692,19 @@
|
||||
"admin.search.item.withdraw": "Poista käytöstä",
|
||||
|
||||
// "admin.search.title": "Administrative Search",
|
||||
"admin.search.title": "Hallinnollinen haku",
|
||||
"admin.search.title": "Ylläpitäjän haku",
|
||||
|
||||
// "administrativeView.search.results.head": "Administrative Search",
|
||||
"administrativeView.search.results.head": "Hallinnollinen haku",
|
||||
"administrativeView.search.results.head": "Ylläpitäjän haku",
|
||||
|
||||
|
||||
|
||||
|
||||
// "admin.workflow.breadcrumbs": "Administer Workflow",
|
||||
"admin.workflow.breadcrumbs": "Hallinnointityönkulku",
|
||||
"admin.workflow.breadcrumbs": "Hallinnoi työnkulkua",
|
||||
|
||||
// "admin.workflow.title": "Administer Workflow",
|
||||
"admin.workflow.title": "Hallinnointityönkulku",
|
||||
"admin.workflow.title": "Hallinnoi työnkulkua",
|
||||
|
||||
// "admin.workflow.item.workflow": "Workflow",
|
||||
"admin.workflow.item.workflow": "Työnkulku",
|
||||
@@ -2954,7 +2954,7 @@
|
||||
|
||||
|
||||
// "menu.section.admin_search": "Admin Search",
|
||||
"menu.section.admin_search": "Admin-haku",
|
||||
"menu.section.admin_search": "Ylläpitäjän haku",
|
||||
|
||||
|
||||
|
||||
@@ -3033,7 +3033,7 @@
|
||||
"menu.section.icon.access_control": "Pääsyoikeudet",
|
||||
|
||||
// "menu.section.icon.admin_search": "Admin search menu section",
|
||||
"menu.section.icon.admin_search": "Admin-haku",
|
||||
"menu.section.icon.admin_search": "Ylläpitäjän haku",
|
||||
|
||||
// "menu.section.icon.control_panel": "Control Panel menu section",
|
||||
"menu.section.icon.control_panel": "Hallintapaneeli",
|
||||
@@ -3168,7 +3168,7 @@
|
||||
|
||||
|
||||
// "menu.section.workflow": "Administer Workflow",
|
||||
"menu.section.workflow": "Hallinnointityönkulku",
|
||||
"menu.section.workflow": "Hallinnoi työnkulkua",
|
||||
|
||||
|
||||
// "mydspace.description": "",
|
||||
@@ -5079,7 +5079,7 @@
|
||||
|
||||
|
||||
// "workflowAdmin.search.results.head": "Administer Workflow",
|
||||
"workflowAdmin.search.results.head": "Hallinnointityönkulku",
|
||||
"workflowAdmin.search.results.head": "Hallinnoi työnkulkua",
|
||||
|
||||
|
||||
|
||||
|
21
src/assets/images/orcid.logo.icon.svg
Normal file
21
src/assets/images/orcid.logo.icon.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: sketchtool 53.1 (72631) - https://sketchapp.com -->
|
||||
<title>Orcid logo</title>
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="hero" transform="translate(-924.000000, -72.000000)" fill-rule="nonzero">
|
||||
<g id="Group-4">
|
||||
<g id="vector_iD_icon" transform="translate(924.000000, 72.000000)">
|
||||
<path d="M72,36 C72,55.884375 55.884375,72 36,72 C16.115625,72 0,55.884375 0,36 C0,16.115625 16.115625,0 36,0 C55.884375,0 72,16.115625 72,36 Z" id="Path" fill="#A6CE39"></path>
|
||||
<g id="Group" transform="translate(18.868966, 12.910345)" fill="#FFFFFF">
|
||||
<polygon id="Path" points="5.03734929 39.1250878 0.695429861 39.1250878 0.695429861 9.14431787 5.03734929 9.14431787 5.03734929 22.6930505 5.03734929 39.1250878"></polygon>
|
||||
<path d="M11.409257,9.14431787 L23.1380784,9.14431787 C34.303014,9.14431787 39.2088191,17.0664074 39.2088191,24.1486995 C39.2088191,31.846843 33.1470485,39.1530811 23.1944669,39.1530811 L11.409257,39.1530811 L11.409257,9.14431787 Z M15.7511765,35.2620194 L22.6587756,35.2620194 C32.49858,35.2620194 34.7541226,27.8438084 34.7541226,24.1486995 C34.7541226,18.1301509 30.8915059,13.0353795 22.4332213,13.0353795 L15.7511765,13.0353795 L15.7511765,35.2620194 Z" id="Shape"></path>
|
||||
<path d="M5.71401206,2.90182329 C5.71401206,4.441452 4.44526937,5.72914146 2.86638958,5.72914146 C1.28750978,5.72914146 0.0187670918,4.441452 0.0187670918,2.90182329 C0.0187670918,1.33420133 1.28750978,0.0745051096 2.86638958,0.0745051096 C4.44526937,0.0745051096 5.71401206,1.36219458 5.71401206,2.90182329 Z" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@@ -116,9 +116,25 @@ ngb-modal-backdrop {
|
||||
margin-left: var(--ds-gap);
|
||||
}
|
||||
|
||||
.custom-accordion .card-header button {
|
||||
-webkit-box-shadow: none!important;
|
||||
box-shadow: none!important;
|
||||
width: 100%;
|
||||
}
|
||||
.custom-accordion .card:first-of-type {
|
||||
border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color) !important;
|
||||
border-bottom-left-radius: var(--bs-card-border-radius) !important;
|
||||
border-bottom-right-radius: var(--bs-card-border-radius) !important;
|
||||
}
|
||||
|
||||
ds-dynamic-form-control-container.d-none {
|
||||
/* Ensures that form-control containers hidden and disabled by type binding collapse and let other fields in
|
||||
the same row expand accordingly
|
||||
*/
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
/* Used for dso administrative functionality */
|
||||
.btn-dark {
|
||||
background-color: var(--ds-admin-sidebar-bg);
|
||||
}
|
||||
|
Reference in New Issue
Block a user