diff --git a/src/app/core/profile/researcher-profile.service.ts b/src/app/core/profile/researcher-profile.service.ts index 3b4144b77c..c3383c845d 100644 --- a/src/app/core/profile/researcher-profile.service.ts +++ b/src/app/core/profile/researcher-profile.service.ts @@ -38,19 +38,19 @@ import {CoreState} from "../core-state.model"; * A private DataService implementation to delegate specific methods to. */ class ResearcherProfileServiceImpl extends DataService { - protected linkPath = 'profiles'; + protected linkPath = 'profiles'; - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - super(); - } + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } } @@ -61,100 +61,178 @@ class ResearcherProfileServiceImpl extends DataService { @dataService(RESEARCHER_PROFILE) export class ResearcherProfileService { - dataService: ResearcherProfileServiceImpl; + dataService: ResearcherProfileServiceImpl; - responseMsToLive: number = 10 * 1000; + responseMsToLive: number = 10 * 1000; - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected router: Router, - protected comparator: DefaultChangeAnalyzer, - protected itemService: ItemDataService, - protected configurationService: ConfigurationDataService ) { + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected router: Router, + protected comparator: DefaultChangeAnalyzer, + protected itemService: ItemDataService, + protected configurationService: ConfigurationDataService ) { - this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, store, objectCache, halService, - notificationsService, http, comparator); + this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, store, objectCache, halService, + notificationsService, http, comparator); - } + } - /** - * Find the researcher profile with the given uuid. - * - * @param uuid the profile uuid - */ - findById(uuid: string): Observable { - return this.dataService.findById(uuid, false) - .pipe ( getFinishedRemoteData(), - map((remoteData) => remoteData.payload)); - } + /** + * Find the researcher profile with the given uuid. + * + * @param uuid the profile uuid + */ + findById(uuid: string): Observable { + return this.dataService.findById(uuid, false) + .pipe ( getFinishedRemoteData(), + map((remoteData) => remoteData.payload)); + } - /** - * Create a new researcher profile for the current user. - */ - create(): Observable> { - return this.dataService.create( new ResearcherProfile()); - } + /** + * Create a new researcher profile for the current user. + */ + create(): Observable> { + return this.dataService.create( new ResearcherProfile()); + } - /** - * Delete a researcher profile. - * - * @param researcherProfile the profile to delete - */ - delete(researcherProfile: ResearcherProfile): Observable { - return this.dataService.delete(researcherProfile.id).pipe( - getFirstCompletedRemoteData(), - tap((response: RemoteData) => { - if (response.isSuccess) { - this.requestService.setStaleByHrefSubstring(researcherProfile._links.self.href); - } + /** + * Delete a researcher profile. + * + * @param researcherProfile the profile to delete + */ + delete(researcherProfile: ResearcherProfile): Observable { + return this.dataService.delete(researcherProfile.id).pipe( + getFirstCompletedRemoteData(), + tap((response: RemoteData) => { + if (response.isSuccess) { + this.requestService.setStaleByHrefSubstring(researcherProfile._links.self.href); + } + }), + map((response: RemoteData) => response.isSuccess) + ); + } + + /** + * Find the item id related to the given researcher profile. + * + * @param researcherProfile the profile to find for + */ + findRelatedItemId( researcherProfile: ResearcherProfile ): Observable { + return this.itemService.findByHref(researcherProfile._links.item.href, false) + .pipe (getFirstSucceededRemoteDataPayload(), + catchError((error) => { + console.debug(error); + return observableOf(null); }), - map((response: RemoteData) => response.isSuccess) - ); - } + map((item) => item != null ? item.id : null )); + } - /** - * Find the item id related to the given researcher profile. - * - * @param researcherProfile the profile to find for - */ - findRelatedItemId( researcherProfile: ResearcherProfile ): Observable { - return this.itemService.findByHref(researcherProfile._links.item.href, false) - .pipe (getFirstSucceededRemoteDataPayload(), - catchError((error) => { - console.debug(error); - return observableOf(null); - }), - map((item) => item != null ? item.id : null )); - } + /** + * Change the visibility of the given researcher profile setting the given value. + * + * @param researcherProfile the profile to update + * @param visible the visibility value to set + */ + setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable { - /** - * Change the visibility of the given researcher profile setting the given value. - * - * @param researcherProfile the profile to update - * @param visible the visibility value to set - */ - setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable { + const replaceOperation: ReplaceOperation = { + path: '/visible', + op: 'replace', + value: visible + }; - const replaceOperation: ReplaceOperation = { - path: '/visible', - op: 'replace', - value: visible - }; + return this.patch(researcherProfile, [replaceOperation]).pipe ( + switchMap( ( ) => this.findById(researcherProfile.id)) + ); + } - return this.patch(researcherProfile, [replaceOperation]).pipe ( - switchMap( ( ) => this.findById(researcherProfile.id)) - ); - } + patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { + return this.dataService.patch(researcherProfile, operations); + } - patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { - return this.dataService.patch(researcherProfile, operations); - } + /** + * Check if the given item is linked to an ORCID profile. + * + * @param item the item to check + * @returns the check result + */ + isLinkedToOrcid(item: Item): boolean { + return item.hasMetadata('cris.orcid.authenticated'); + } + + /** + * Returns true if only the admin users can disconnect a researcher profile from ORCID. + * + * @returns the check result + */ + onlyAdminCanDisconnectProfileFromOrcid(): Observable { + return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( + map((property) => property.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 + */ + ownerCanDisconnectProfileFromOrcid(): Observable { + return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( + map((property) => { + const values = property.values.map( (value) => value.toLowerCase()); + return values.includes('only_owner') || values.includes('admin_and_owner'); + }) + ); + } + + /** + * Returns true if the admin users can disconnect a researcher profile from ORCID. + * + * @returns the check result + */ + adminCanDisconnectProfileFromOrcid(): Observable { + return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( + map((property) => { + const values = property.values.map( (value) => value.toLowerCase()); + return values.includes('only_admin') || values.includes('admin_and_owner'); + }) + ); + } + + /** + * If the given item represents a profile unlink it from ORCID. + */ + unlinkOrcid(item: Item): Observable> { + + const operations: RemoveOperation[] = [{ + path:'/orcid', + op:'remove' + }]; + + return this.findById(item.firstMetadata('cris.owner').authority).pipe( + switchMap((profile) => this.patch(profile, operations)), + getFinishedRemoteData() + ); + } + + getOrcidAuthorizeUrl(profile: Item): Observable { + return combineLatest([ + this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()), + this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()), + this.configurationService.findByPropertyName('orcid.scope').pipe(getFirstSucceededRemoteDataPayload())] + ).pipe( + map(([authorizeUrl, clientId, scopes]) => { + const redirectUri = environment.rest.baseUrl + '/api/cris/orcid/' + profile.id + '/?url=' + encodeURIComponent(this.router.url); + 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 @@ -180,4 +258,10 @@ export class ResearcherProfileService { return this.rdbService.buildFromRequestUUID(requestId); } + private getOrcidDisconnectionAllowedUsersConfiguration(): Observable { + return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe( + getFirstSucceededRemoteDataPayload() + ); + } + } diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index f584164c97..2c4b57b249 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -37,6 +37,7 @@ import { ThemedFileSectionComponent } from './simple/field-components/file-secti 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 { OrcidSettingComponent } from './orcid-page/orcid-sync/orcid-setting.component'; const ENTRY_COMPONENTS = [ @@ -71,7 +72,8 @@ const DECLARATIONS = [ MiradorViewerComponent, VersionPageComponent, OrcidPageComponent, - OrcidAuthComponent + OrcidAuthComponent, + OrcidSettingComponent ]; @NgModule({ diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index ba9f445ec2..4e62a8d51c 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1 +1,2 @@ + diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html new file mode 100644 index 0000000000..793e7570ed --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html @@ -0,0 +1,82 @@ +
+ + + +
+
+
+ {{ 'person.page.orcid.synchronization-mode-message' | translate}} +
+
+
+
{{ 'person.page.orcid.synchronization-mode'| translate }}
+
+
+
+ + +
+
+
+
+
+
+
+
{{ 'person.page.orcid.publications-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
{{ 'person.page.orcid.funding-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
{{ 'person.page.orcid.profile-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts new file mode 100644 index 0000000000..87385f0780 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Operation } from 'fast-json-patch'; +import { switchMap } from 'rxjs/operators'; +import { AuthService } from '../../../core/auth/auth.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { Item } from '../../../core/shared/item.model'; +import { getFinishedRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; + +@Component({ + selector: 'ds-orcid-setting', + templateUrl: './orcid-setting.component.html', + styleUrls: ['./orcid-setting.component.scss'] +}) +export class OrcidSettingComponent implements OnInit { + + messagePrefix = 'person.page.orcid'; + + currentSyncMode: string; + + currentSyncPublications: string; + + currentSyncFundings: string; + + syncModes: { value: string, label: string }[]; + + syncPublicationOptions: { value: string, label: string }[]; + + syncFundingOptions: {value: string, label: string}[]; + + syncProfileOptions: { value: string, label: string, checked: boolean }[]; + + item: Item; + + constructor(private researcherProfileService: ResearcherProfileService, + protected translateService: TranslateService, + private notificationsService: NotificationsService, + public authService: AuthService, + private route: ActivatedRoute, + private itemService: ItemDataService + ) { + this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { + this.item = data.payload; + }); + } + + 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('cris.orcid.sync-profile'); + + this.syncProfileOptions = ['AFFILIATION', 'EDUCATION', 'BIOGRAPHICAL', 'IDENTIFIERS'] + .map((value) => { + return { + label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), + value: value, + checked: syncProfilePreferences.includes(value) + }; + }); + + this.currentSyncMode = this.getCurrentPreference('cris.orcid.sync-mode', ['BATCH, MANUAL'], 'MANUAL'); + this.currentSyncPublications = this.getCurrentPreference('cris.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); + this.currentSyncFundings = this.getCurrentPreference('cris.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + } + + onSubmit(form: FormGroup) { + 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.findById(this.item.firstMetadata('cris.owner').authority).pipe( + switchMap((profile) => this.researcherProfileService.patch(profile, operations)), + getFinishedRemoteData() + ).subscribe((remoteData) => { + if (remoteData.isSuccess) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error')); + } + }); + } + + fillOperationsFor(operations: Operation[], path: string, currentValue: string) { + operations.push({ + path: path, + op: 'replace', + value: currentValue + }); + } + + getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { + const currentPreference = this.item.firstMetadataValue(metadataField); + return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; + } + + +} diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts index df234bcbb4..0bc9e18520 100644 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts @@ -7,7 +7,6 @@ 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 { CoreState } from '../../../../core/core.reducers'; import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; import { isNotNull, isEmpty } from '../../../empty.util'; @@ -15,6 +14,7 @@ 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"; @Component({ selector: 'ds-log-in-orcid',