diff --git a/backend/core/pom.xml b/backend/core/pom.xml index 6f168c8a7..da86e1101 100644 --- a/backend/core/pom.xml +++ b/backend/core/pom.xml @@ -95,6 +95,11 @@ oidc-authn 2.2.2 + + org.opencdmp + evaluator-base + 0.0.2 + gr.cite diff --git a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java index c8d18b37d..19f17d56a 100644 --- a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java +++ b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java @@ -155,6 +155,8 @@ public class AuditableAction { public static final EventId FileTransformer_GetAvailableConfigurations = new EventId(20000, "FileTransformer_GetAvailableConfigurations"); + public static final EventId Evaluator_GetAvailableConfigurations = new EventId(20001, "Evaluator_GetAvailableConfigurations"); + public static final EventId ContactSupport_Sent = new EventId(210000, "ContactSupport_Sent"); public static final EventId ContactSupport_PublicSent = new EventId(210001, "ContactSupport_PublicSent"); diff --git a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java index da34e8d1d..c2888f6fb 100644 --- a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java +++ b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java @@ -89,6 +89,7 @@ public final class Permission { public static String AssignPlanUsers = "AssignPlanUsers"; public static String InvitePlanUsers = "InvitePlanUsers"; public static String AnnotatePlan = "AnnotatePlan"; + public static String EvaluatePlan = "EvaluatePlan"; //PlanStatus public static String BrowsePlanStatus = "BrowsePlanStatus"; @@ -137,6 +138,7 @@ public final class Permission { public static String DeleteDescription = "DeleteDescription"; public static String CloneDescription = "CloneDescription"; public static String ExportDescription = "ExportDescription"; + public static String EvaluateDescription = "EvaluateDescription"; //DescriptionTag public static String BrowseDescriptionTag = "BrowseDescriptionTag"; diff --git a/backend/core/src/main/java/org/opencdmp/commons/enums/TenantConfigurationType.java b/backend/core/src/main/java/org/opencdmp/commons/enums/TenantConfigurationType.java index 1bfa0dfff..e84a6aed3 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/enums/TenantConfigurationType.java +++ b/backend/core/src/main/java/org/opencdmp/commons/enums/TenantConfigurationType.java @@ -11,7 +11,8 @@ public enum TenantConfigurationType implements DatabaseEnum { FileTransformerPlugins((short) 1), DefaultUserLocale((short) 2), Logo((short) 3), - CssColors((short) 4); + CssColors((short) 4), + EvaluatorPlugins((short) 5); private final Short value; diff --git a/backend/core/src/main/java/org/opencdmp/commons/types/evaluator/EvaluatorSourceEntity.java b/backend/core/src/main/java/org/opencdmp/commons/types/evaluator/EvaluatorSourceEntity.java new file mode 100644 index 000000000..2b922b567 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/commons/types/evaluator/EvaluatorSourceEntity.java @@ -0,0 +1,68 @@ +package org.opencdmp.commons.types.evaluator; + +public class EvaluatorSourceEntity { + + private String url; + private String evaluatorId; + private String issuerUrl; + private String clientId; + private String clientSecret; + private String scope; + private int maxInMemorySizeInBytes; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getEvaluatorId() { + return evaluatorId; + } + + public void setEvaluatorId(String evaluatorId) { + this.evaluatorId = evaluatorId; + } + + public String getIssuerUrl() { + return issuerUrl; + } + + public void setIssuerUrl(String issuerUrl) { + this.issuerUrl = issuerUrl; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public int getMaxInMemorySizeInBytes() { + return maxInMemorySizeInBytes; + } + + public void setMaxInMemorySizeInBytes(int maxInMemorySizeInBytes) { + this.maxInMemorySizeInBytes = maxInMemorySizeInBytes; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/commons/types/tenantconfiguration/EvaluatorTenantConfigurationEntity.java b/backend/core/src/main/java/org/opencdmp/commons/types/tenantconfiguration/EvaluatorTenantConfigurationEntity.java new file mode 100644 index 000000000..10a318c25 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/commons/types/tenantconfiguration/EvaluatorTenantConfigurationEntity.java @@ -0,0 +1,26 @@ +package org.opencdmp.commons.types.tenantconfiguration; + +import org.opencdmp.commons.types.evaluator.EvaluatorSourceEntity; + +import java.util.List; + +public class EvaluatorTenantConfigurationEntity { + + private List sources; + private boolean disableSystemSources; + + public List getSources() { + return sources; + } + + public void setSources(List sources) { + this.sources = sources; + } + + public boolean getDisableSystemSources(){ + return disableSystemSources; + } + public void setDisableSystemSources(boolean disableSystemSources) { + this.disableSystemSources = disableSystemSources; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/commonmodels/plan/PlanCommonModelBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/commonmodels/plan/PlanCommonModelBuilder.java index 1a4ace692..74968a139 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/commonmodels/plan/PlanCommonModelBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/commonmodels/plan/PlanCommonModelBuilder.java @@ -61,6 +61,7 @@ public class PlanCommonModelBuilder extends BaseCommonModelBuilder authorize = EnumSet.of(AuthorizationFlags.None); @@ -96,6 +97,11 @@ public class PlanCommonModelBuilder extends BaseCommonModelBuilder evaluatorEntityTypes; + private boolean useSharedStorage; + private boolean hasLogo; + + public String getEvaluatorId() { + return evaluatorId; + } + + public void setEvaluatorId(String evaluatorId) { + this.evaluatorId = evaluatorId; + } + + public RankConfig getRankConfig() { + return rankConfig; + } + + public void setRankConfig(RankConfig rankConfig) { + this.rankConfig = rankConfig; + } + + public List getEvaluatorEntityTypes() { + return evaluatorEntityTypes; + } + + public void setEvaluatorEntityTypes(List evaluatorEntityTypes) { + this.evaluatorEntityTypes = evaluatorEntityTypes; + } + + public boolean isUseSharedStorage() { + return useSharedStorage; + } + + public void setUseSharedStorage(boolean useSharedStorage) { + this.useSharedStorage = useSharedStorage; + } + + public boolean isHasLogo() { + return hasLogo; + } + + public void setHasLogo(boolean hasLogo) { + this.hasLogo = hasLogo; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/evaluator/EvaluatorSource.java b/backend/core/src/main/java/org/opencdmp/model/evaluator/EvaluatorSource.java new file mode 100644 index 000000000..785fd3b74 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/evaluator/EvaluatorSource.java @@ -0,0 +1,76 @@ +package org.opencdmp.model.evaluator; + +public class EvaluatorSource { + + private String url; + + public static final String _url = "url"; + + private String evaluatorId; + + public static final String _evaluatorId = "evaluatorId"; + + private String issuerUrl; + + public static final String _issuerUrl = "issuerUrl"; + + private String clientId; + + public static final String _clientId = "clientId"; + + private String clientSecret; + + public static final String _clientSecret = "clientSecret"; + + private String scope; + + public static final String _scope = "scope"; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getEvaluatorId() { + return evaluatorId; + } + + public void setEvaluatorId(String evaluatorId) { + this.evaluatorId = evaluatorId; + } + + public String getIssuerUrl() { + return issuerUrl; + } + + public void setIssuerUrl(String issuerUrl) { + this.issuerUrl = issuerUrl; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/tenantconfiguration/TenantConfiguration.java b/backend/core/src/main/java/org/opencdmp/model/tenantconfiguration/TenantConfiguration.java index bb7826f4c..0d7240bb8 100644 --- a/backend/core/src/main/java/org/opencdmp/model/tenantconfiguration/TenantConfiguration.java +++ b/backend/core/src/main/java/org/opencdmp/model/tenantconfiguration/TenantConfiguration.java @@ -2,6 +2,7 @@ package org.opencdmp.model.tenantconfiguration; import org.opencdmp.commons.enums.IsActive; import org.opencdmp.commons.enums.TenantConfigurationType; +import org.opencdmp.commons.types.tenantconfiguration.EvaluatorTenantConfigurationEntity; import java.time.Instant; import java.util.UUID; @@ -36,6 +37,10 @@ public class TenantConfiguration { public static final String _fileTransformerPlugins = "fileTransformerPlugins"; + private EvaluatorTenantConfigurationEntity evaluatorPlugins; + + public static final String _evaluatorPlugins = "evaluatorPlugins"; + private LogoTenantConfiguration logo; public static final String _logo = "logo"; @@ -150,4 +155,12 @@ public class TenantConfiguration { public void setType(TenantConfigurationType type) { this.type = type; } + + public EvaluatorTenantConfigurationEntity getEvaluatorPlugins() { + return evaluatorPlugins; + } + + public void setEvaluatorPlugins(EvaluatorTenantConfigurationEntity evaluatorPlugins) { + this.evaluatorPlugins = evaluatorPlugins; + } } diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorClientImpl.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorClientImpl.java new file mode 100644 index 000000000..1c387d538 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorClientImpl.java @@ -0,0 +1,58 @@ +package org.opencdmp.service.evaluator; + +import com.sun.jdi.InvalidTypeException; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import org.opencdmp.commonmodels.models.description.DescriptionModel; +import org.opencdmp.commonmodels.models.plan.PlanModel; +import org.opencdmp.evaluatorbase.interfaces.EvaluatorClient; +import org.opencdmp.evaluatorbase.interfaces.EvaluatorConfiguration; +import org.opencdmp.evaluatorbase.models.misc.RankModel; +import org.opencdmp.service.filetransformer.FileTransformerRepository; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.management.InvalidApplicationException; +import java.io.IOException; + +public class EvaluatorClientImpl implements EvaluatorClient { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(FileTransformerRepository.class)); + + private final WebClient transformerClient; + + public EvaluatorClientImpl(WebClient transformerClient) { + this.transformerClient = transformerClient; + } + + @Override + public RankModel rankPlan(PlanModel planModel) throws InvalidApplicationException, IOException, InvalidTypeException { + logger.debug(new MapLogEntry("rankPlan").And("planModel", planModel)); + + return this.transformerClient.post().uri("/rank/plan").bodyValue(planModel) // Send planModel in the body + .exchangeToMono(mono -> mono.statusCode().isError() ? mono.createException().flatMap(Mono::error) : mono.bodyToMono(RankModel.class)).block(); + } + + @Override + public RankModel rankDescription(DescriptionModel descriptionModel) throws InvalidApplicationException, IOException { + logger.debug(new MapLogEntry("rankDescription").And("descriptionModel", descriptionModel)); + return this.transformerClient.post().uri("/rank/description").bodyValue(descriptionModel) // Send descriptionModel in the body + .exchangeToMono(mono -> mono.statusCode().isError() ? mono.createException().flatMap(Mono::error) : mono.bodyToMono(RankModel.class)).block(); + + } + + @Override + public EvaluatorConfiguration getConfiguration() { + logger.debug(new MapLogEntry("getConfiguration")); + return this.transformerClient.get().uri("/config") + .exchangeToMono(mono -> mono.statusCode().isError() ? mono.createException().flatMap(Mono::error) : mono.bodyToMono(new ParameterizedTypeReference() {})).block(); + } + + @Override + public String getLogo() { + logger.debug(new MapLogEntry("getLogo")); + return this.transformerClient.get().uri("/logo").exchangeToMono(mono -> mono.statusCode().isError() ? mono.createException().flatMap(Mono::error) : mono.bodyToMono(String.class)).block(); + } +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfiguration.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfiguration.java new file mode 100644 index 000000000..93b0c7e34 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfiguration.java @@ -0,0 +1,9 @@ +package org.opencdmp.service.evaluator; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties({EvaluatorProperties.class}) +public class EvaluatorConfiguration { +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfigurationCacheOptions.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfigurationCacheOptions.java new file mode 100644 index 000000000..0f31da03b --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfigurationCacheOptions.java @@ -0,0 +1,10 @@ +package org.opencdmp.service.evaluator; + +import gr.cite.tools.cache.CacheOptions; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "cache.evaluator-config-by-id") +public class EvaluatorConfigurationCacheOptions extends CacheOptions { +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfigurationCacheService.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfigurationCacheService.java new file mode 100644 index 000000000..458dd83a3 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorConfigurationCacheService.java @@ -0,0 +1,76 @@ +package org.opencdmp.service.evaluator; + +import gr.cite.tools.cache.CacheService; +import org.opencdmp.evaluatorbase.interfaces.EvaluatorConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; + +@Service +public class EvaluatorConfigurationCacheService extends CacheService { + + public static class EvaluatorConfigurationCacheValue { + + public EvaluatorConfigurationCacheValue() { + } + + public EvaluatorConfigurationCacheValue(String repositoryId, String tenantCode, EvaluatorConfiguration configuration) { + this.evaluatorId = repositoryId; + this.configuration = configuration; + this.tenantCode = tenantCode == null ? "" : tenantCode; + } + + private String evaluatorId; + private String tenantCode; + + public String getEvaluatorId() { + return evaluatorId; + } + + public void setEvaluatorId(String evaluatorId) { + this.evaluatorId = evaluatorId; + } + + private EvaluatorConfiguration configuration; + + public EvaluatorConfiguration getConfiguration() { + return configuration; + } + + public void setConfiguration(EvaluatorConfiguration configuration) { + this.configuration = configuration; + } + + public String getTenantCode() { + return tenantCode; + } + + public void setTenantCode(String tenantCode) { + this.tenantCode = tenantCode; + } + } + + @Autowired + public EvaluatorConfigurationCacheService(EvaluatorConfigurationCacheOptions options) { + super(options); + } + + @Override + protected Class valueClass() { + return EvaluatorConfigurationCacheService.EvaluatorConfigurationCacheValue.class; + } + + @Override + public String keyOf(EvaluatorConfigurationCacheService.EvaluatorConfigurationCacheValue value) { + return this.buildKey(value.getEvaluatorId(), value.getTenantCode()); + } + + + public String buildKey(String evaluatorId, String tenantCod) { + HashMap keyParts = new HashMap<>(); + keyParts.put("$evaluatorId$", evaluatorId); + keyParts.put("$tenantCode$", tenantCod); + return this.generateKey(keyParts); + } +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorProperties.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorProperties.java new file mode 100644 index 000000000..1781a19c0 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorProperties.java @@ -0,0 +1,21 @@ +package org.opencdmp.service.evaluator; + +import org.opencdmp.commons.types.evaluator.EvaluatorSourceEntity; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@ConfigurationProperties(prefix = "evaluator") +public class EvaluatorProperties { + + private List sources; + + public List getSources() { + return sources; + } + + public void setSources(List sources) { + this.sources = sources; + } +} + diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorService.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorService.java new file mode 100644 index 000000000..5c4b1b050 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorService.java @@ -0,0 +1,26 @@ +package org.opencdmp.service.evaluator; + +import com.sun.jdi.InvalidTypeException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.management.InvalidApplicationException; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.UUID; + +public interface EvaluatorService { + + org.opencdmp.evaluatorbase.models.misc.RankModel rankPlan(UUID planId, String repositoryId, String format, boolean isPublic) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, IOException, InvalidTypeException; + + org.opencdmp.evaluatorbase.models.misc.RankModel rankDescription(UUID descriptionId, String repositoryId, String format, boolean isPublic) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, IOException; + + List getAvailableEvaluators() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException; + + String getLogo(String evaluatorId) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException; + +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorServiceImpl.java new file mode 100644 index 000000000..761f492b0 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorServiceImpl.java @@ -0,0 +1,326 @@ +package org.opencdmp.service.evaluator; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sun.jdi.InvalidTypeException; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.commons.web.oidc.filter.webflux.TokenExchangeCacheService; +import gr.cite.commons.web.oidc.filter.webflux.TokenExchangeFilterFunction; +import gr.cite.commons.web.oidc.filter.webflux.TokenExchangeModel; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.fieldset.BaseFieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import org.opencdmp.authorization.AuthorizationFlags; +import org.opencdmp.authorization.Permission; +import org.opencdmp.commonmodels.models.description.DescriptionModel; +import org.opencdmp.commons.JsonHandlingService; +import org.opencdmp.commons.enums.*; +import org.opencdmp.commons.scope.tenant.TenantScope; +import org.opencdmp.commons.types.evaluator.EvaluatorSourceEntity; +import org.opencdmp.depositbase.repository.DepositClient; +import org.opencdmp.evaluatorbase.interfaces.EvaluatorClient; +import org.opencdmp.evaluatorbase.interfaces.EvaluatorConfiguration; +import org.opencdmp.commons.types.tenantconfiguration.EvaluatorTenantConfigurationEntity; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.data.DescriptionEntity; +import org.opencdmp.data.PlanEntity; +import org.opencdmp.data.TenantConfigurationEntity; +import org.opencdmp.data.TenantEntityManager; +import org.opencdmp.evaluatorbase.models.misc.RankModel; +import org.opencdmp.event.TenantConfigurationTouchedEvent; +import org.opencdmp.model.builder.commonmodels.DepositConfigurationBuilder; +import org.opencdmp.model.builder.commonmodels.description.DescriptionCommonModelBuilder; +import org.opencdmp.model.builder.commonmodels.plan.PlanCommonModelBuilder; +import org.opencdmp.model.description.Description; +import org.opencdmp.model.plan.Plan; +import org.opencdmp.commonmodels.models.plan.PlanModel; +import org.opencdmp.model.tenantconfiguration.TenantConfiguration; +import org.opencdmp.query.DescriptionQuery; +import org.opencdmp.query.PlanQuery; +import org.opencdmp.query.TenantConfigurationQuery; +import org.opencdmp.service.accounting.AccountingService; +import org.opencdmp.service.encryption.EncryptionService; +import org.opencdmp.service.tenant.TenantProperties; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.event.EventListener; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.management.InvalidApplicationException; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static org.opencdmp.authorization.AuthorizationFlags.Public; + +@Service +public class EvaluatorServiceImpl implements EvaluatorService { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(EvaluatorServiceImpl.class)); + private final EvaluatorProperties evaluatorProperties; + private final Map clients; + private final TokenExchangeCacheService tokenExchangeCacheService; + private final EvaluatorConfigurationCacheService evaluatorConfigurationCacheService; + private final AuthorizationService authorizationService; + private final QueryFactory queryFactory; + private final BuilderFactory builderFactory; + private final MessageSource messageSource; + private final ConventionService conventionService; + private final TenantScope tenantScope; + private final EncryptionService encryptionService; + private final TenantProperties tenantProperties; + private final JsonHandlingService jsonHandlingService; + private final EvaluatorSourcesCacheService evaluatorSourcesCacheService; + private final AccountingService accountingService; + private final TenantEntityManager entityManager; + + @Autowired + public EvaluatorServiceImpl(EvaluatorProperties evaluatorProperties, Map clients, TokenExchangeCacheService tokenExchangeCacheService, EvaluatorConfigurationCacheService evaluatorConfigurationCacheService, AuthorizationService authorizationService, QueryFactory queryFactory, BuilderFactory builderFactory, MessageSource messageSource, ConventionService conventionService, TenantScope tenantScope, EncryptionService encryptionService, TenantProperties tenantProperties, JsonHandlingService jsonHandlingService, EvaluatorSourcesCacheService evaluatorSourcesCacheService, AccountingService accountingService, TenantEntityManager entityManager) { + this.evaluatorProperties = evaluatorProperties; + this.clients = clients; + this.tokenExchangeCacheService = tokenExchangeCacheService; + this.evaluatorConfigurationCacheService = evaluatorConfigurationCacheService; + this.authorizationService = authorizationService; + this.queryFactory = queryFactory; + this.builderFactory = builderFactory; + this.messageSource = messageSource; + this.conventionService = conventionService; + this.tenantScope = tenantScope; + this.encryptionService = encryptionService; + this.tenantProperties = tenantProperties; + this.jsonHandlingService = jsonHandlingService; + this.evaluatorSourcesCacheService = evaluatorSourcesCacheService; + this.accountingService = accountingService; + this.entityManager = entityManager; + } + private EvaluatorClientImpl getEvaluatorClient(String repoId) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + String repositoryIdByTenant = this.getRepositoryIdByTenant(repoId); + if (this.clients.containsKey(repositoryIdByTenant)) { + return this.clients.get(repositoryIdByTenant); + } + + EvaluatorSourceEntity source = this.getEvaluatorSources().stream() + .filter(evaluatorSourceEntity -> evaluatorSourceEntity.getEvaluatorId().equals(repoId)) + .findFirst().orElse(null); + + try { + TokenExchangeModel tokenExchangeModel = new TokenExchangeModel( + "evaluator:" + repositoryIdByTenant, + source.getIssuerUrl(), + source.getClientId(), + source.getClientSecret(), + source.getScope() + ); + + TokenExchangeFilterFunction tokenExchangeFilterFunction = new TokenExchangeFilterFunction( + this.tokenExchangeCacheService, + tokenExchangeModel + ); + + EvaluatorClientImpl repository = new EvaluatorClientImpl( + WebClient.builder().baseUrl(source.getUrl() + "/api/evaluator") + .filters(exchangeFilterFunctions -> { + exchangeFilterFunctions.add(tokenExchangeFilterFunction); + exchangeFilterFunctions.add(logRequest()); + exchangeFilterFunctions.add(logResponse()); + }) + .codecs(codecs -> { + codecs.defaultCodecs().maxInMemorySize(source.getMaxInMemorySizeInBytes()); + codecs.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper().registerModule(new JavaTimeModule()), MediaType.APPLICATION_JSON)); + codecs.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(new ObjectMapper().registerModule(new JavaTimeModule()), MediaType.APPLICATION_JSON)); + }) + .build() + ); + + this.clients.put(repositoryIdByTenant, repository); + return repository; + } catch (Exception e) { + logger.error("Exception occurred while creating EvaluatorClientImpl", e); + return null; + } + } + + + private List getEvaluatorSources() throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + String tenantCode = this.tenantScope.isSet() && this.tenantScope.isMultitenant() ? this.tenantScope.getTenantCode() : ""; + EvaluatorSourcesCacheService.EvaluatorSourceCacheValue cacheValue = this.evaluatorSourcesCacheService.lookup(this.evaluatorSourcesCacheService.buildKey(tenantCode)); + if (cacheValue == null) { + List evaluatorSourceEntities = new ArrayList<>(this.evaluatorProperties.getSources()); + + if (this.tenantScope.isSet() && this.tenantScope.isMultitenant()) { + TenantConfigurationQuery tenantConfigurationQuery = this.queryFactory.query(TenantConfigurationQuery.class).disableTracking().isActive(IsActive.Active).types(TenantConfigurationType.EvaluatorPlugins); + if (this.tenantScope.isDefaultTenant()) tenantConfigurationQuery.tenantIsSet(false); + else tenantConfigurationQuery.tenantIsSet(true).tenantIds(this.tenantScope.getTenant()); + TenantConfigurationEntity tenantConfiguration = tenantConfigurationQuery.firstAs(new BaseFieldSet().ensure(TenantConfiguration._evaluatorPlugins)); + + if (tenantConfiguration != null && !this.conventionService.isNullOrEmpty(tenantConfiguration.getValue())) { + EvaluatorTenantConfigurationEntity evaluatorTenantConfigurationEntity = this.jsonHandlingService.fromJsonSafe(EvaluatorTenantConfigurationEntity.class, tenantConfiguration.getValue()); + if (evaluatorTenantConfigurationEntity != null) { + if (evaluatorTenantConfigurationEntity.getDisableSystemSources()) evaluatorSourceEntities = new ArrayList<>(); + evaluatorSourceEntities.addAll(this.buildEvaluatorSourceItems(evaluatorTenantConfigurationEntity.getSources())); + } + } + } + cacheValue = new EvaluatorSourcesCacheService.EvaluatorSourceCacheValue(tenantCode, evaluatorSourceEntities); + this.evaluatorSourcesCacheService.put(cacheValue); + } + return cacheValue.getSources(); + } + + @EventListener + public void handleTenantConfigurationTouchedEvent(TenantConfigurationTouchedEvent event) { + if (!event.getType().equals(TenantConfigurationType.FileTransformerPlugins)) return; + EvaluatorSourcesCacheService.EvaluatorSourceCacheValue evaluatorSourceCacheValue = this.evaluatorSourcesCacheService.lookup(this.evaluatorSourcesCacheService.buildKey(event.getTenantCode())); + if (evaluatorSourceCacheValue != null && evaluatorSourceCacheValue.getSources() != null){ + for (EvaluatorSourceEntity source : evaluatorSourceCacheValue.getSources()){ + String repositoryIdByTenant = source.getEvaluatorId() + "_" + event.getTenantCode(); + this.clients.remove(repositoryIdByTenant); + this.evaluatorConfigurationCacheService.evict(this.evaluatorConfigurationCacheService.buildKey(source.getEvaluatorId(), event.getTenantCode())); + } + } + this.evaluatorConfigurationCacheService.evict(this.evaluatorSourcesCacheService.buildKey(event.getTenantCode())); + } + + private List buildEvaluatorSourceItems(List sources) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + List items = new ArrayList<>(); + if (this.conventionService.isListNullOrEmpty(sources)) return items; + for (EvaluatorSourceEntity source : sources){ + EvaluatorSourceEntity item = new EvaluatorSourceEntity(); + item.setEvaluatorId(source.getEvaluatorId()); + item.setUrl(source.getUrl()); + item.setIssuerUrl(source.getIssuerUrl()); + item.setClientId(source.getClientId()); + if (!this.conventionService.isNullOrEmpty(source.getClientSecret())) item.setClientSecret(this.encryptionService.decryptAES(source.getClientSecret(), this.tenantProperties.getConfigEncryptionAesKey(), this.tenantProperties.getConfigEncryptionAesIv())); + item.setScope(source.getScope()); + items.add(item); + } + return items; + } + + private String getRepositoryIdByTenant(String repositoryId) throws InvalidApplicationException { + if (this.tenantScope.isSet() && this.tenantScope.isMultitenant()) { + return repositoryId + "_" + this.tenantScope.getTenantCode(); + } else { + return repositoryId; + } + } + + @Override + public List getAvailableEvaluators() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + this.authorizationService.authorizeForce(Permission.BrowsePlan, Permission.DeferredAffiliation); + List configurations = new ArrayList<>(); + + for(EvaluatorSourceEntity evaluatorSource : this.getEvaluatorSources()){ + + String tenantCode = this.tenantScope.isSet() && this.tenantScope.isMultitenant() ? this.tenantScope.getTenantCode() : ""; + EvaluatorConfigurationCacheService.EvaluatorConfigurationCacheValue cacheValue = this.evaluatorConfigurationCacheService.lookup(this.evaluatorConfigurationCacheService.buildKey(evaluatorSource.getEvaluatorId(), tenantCode)); + + if(cacheValue == null){ + try{ + EvaluatorClientImpl evaluatorClient = this.getEvaluatorClient(evaluatorSource.getEvaluatorId()); + if(evaluatorClient == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{evaluatorSource.getEvaluatorId(), EvaluatorClientImpl.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + EvaluatorConfiguration configuration = evaluatorClient.getConfiguration(); + cacheValue = new EvaluatorConfigurationCacheService.EvaluatorConfigurationCacheValue(evaluatorSource.getEvaluatorId(), tenantCode, configuration); + this.evaluatorConfigurationCacheService.put(cacheValue); + }catch (Exception e){ + logger.error(e.getMessage(), e); + } + } + if(cacheValue != null){ + configurations.add(cacheValue.getConfiguration()); + } + } + + return configurations; + } + + @Override + public RankModel rankPlan(UUID planId, String evaluatorId, String format, boolean isPublic) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, IOException, InvalidTypeException { + this.authorizationService.authorizeForce(Permission.EvaluatePlan); + EvaluatorClientImpl repository = this.getEvaluatorClient(evaluatorId); + + if(repository == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{format, EvaluatorClientImpl.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + PlanEntity planEntity = this.queryFactory.query(PlanQuery.class).disableTracking().ids(planId).first(); + if (planEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{planId, PlanEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + PlanModel evaluatorModel = this.builderFactory.builder(PlanCommonModelBuilder.class).useSharedStorage(repository.getConfiguration().isUseSharedStorage()).setEvaluatorId(repository.getConfiguration().getEvaluatorId()).isPublic(isPublic).authorize(AuthorizationFlags.All).build(planEntity); + if(evaluatorModel == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{planId, Plan.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + this.accountingService.increase(UsageLimitTargetMetric.FILE_TRANSFORMER_EXPORT_PLAN_EXECUTION_COUNT.getValue()); + this.increaseTargetMetricWithRepositoryId(UsageLimitTargetMetric.FILE_TRANSFORMER_EXPORT_PLAN_EXECUTION_COUNT_FOR, evaluatorId); + + return repository.rankPlan(evaluatorModel); + } + + @Override + public RankModel rankDescription(UUID descriptionId, String repositoryId, String format, boolean isPublic) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, IOException { + this.authorizationService.authorizeForce(Permission.EvaluateDescription); + EvaluatorClientImpl repository = this.getEvaluatorClient(repositoryId); + + if(repository == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{format, EvaluatorClientImpl.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + DescriptionEntity descriptionEntity = this.queryFactory.query(DescriptionQuery.class).disableTracking().ids(descriptionId).first(); + if (descriptionEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{descriptionId, DescriptionEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + DescriptionModel descriptionEvaluatorModel = this.builderFactory.builder(DescriptionCommonModelBuilder.class).setRepositoryId(repository.getConfiguration().getEvaluatorId()).useSharedStorage(repository.getConfiguration().isUseSharedStorage()).isPublic(isPublic).authorize(AuthorizationFlags.All).build(descriptionEntity); + if (descriptionEvaluatorModel == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{descriptionId, Description.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + this.accountingService.increase(UsageLimitTargetMetric.FILE_TRANSFORMER_EXPORT_DESCRIPTIONS_EXECUTION_COUNT.getValue()); + this.increaseTargetMetricWithRepositoryId(UsageLimitTargetMetric.FILE_TRANSFORMER_EXPORT_DESCRIPTIONS_EXECUTION_COUNT_FOR, repositoryId); + + return repository.rankDescription(descriptionEvaluatorModel); + } + + @Override + public String getLogo(String evaluatorId) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + this.authorizationService.authorizeForce(Permission.BrowseDeposit, Permission.DeferredAffiliation); + EvaluatorClient evaluatorClient = this.getEvaluatorClient(evaluatorId); + if(evaluatorClient == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{evaluatorId, EvaluatorClient.class.getSimpleName()}, LocaleContextHolder.getLocale())); + return evaluatorClient.getLogo(); + } + + private static ExchangeFilterFunction logRequest() { + return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { + logger.debug(new MapLogEntry("Request").And("method", clientRequest.method().toString()).And("url", clientRequest.url())); + return Mono.just(clientRequest); + }); + } + + private static ExchangeFilterFunction logResponse() { + return ExchangeFilterFunction.ofResponseProcessor(response -> { + if (response.statusCode().isError()) { + return response.mutate().build().bodyToMono(String.class) + .flatMap(body -> { + logger.error(new MapLogEntry("Response").And("method", response.request().getMethod().toString()).And("url", response.request().getURI()).And("status", response.statusCode().toString()).And("body", body)); + return Mono.just(response); + }); + } + return Mono.just(response); + + }); + } + + private void increaseTargetMetricWithRepositoryId(UsageLimitTargetMetric metric, String repositoryId) throws InvalidApplicationException { + this.accountingService.increase(metric.getValue() + repositoryId); + } +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorSourcesCacheOptions.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorSourcesCacheOptions.java new file mode 100644 index 000000000..1261aaad2 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorSourcesCacheOptions.java @@ -0,0 +1,10 @@ +package org.opencdmp.service.evaluator; + +import gr.cite.tools.cache.CacheOptions; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "cache.evaluator-sources-by-tenant") +public class EvaluatorSourcesCacheOptions extends CacheOptions { +} diff --git a/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorSourcesCacheService.java b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorSourcesCacheService.java new file mode 100644 index 000000000..7475be717 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/evaluator/EvaluatorSourcesCacheService.java @@ -0,0 +1,66 @@ +package org.opencdmp.service.evaluator; + +import gr.cite.tools.cache.CacheService; +import org.opencdmp.commons.types.evaluator.EvaluatorSourceEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; + +@Service +public class EvaluatorSourcesCacheService extends CacheService { + + public static class EvaluatorSourceCacheValue { + + public EvaluatorSourceCacheValue() { + } + + public EvaluatorSourceCacheValue(String tenantCode, List sources) { + this.tenantCode = tenantCode; + this.sources = sources; + } + + private String tenantCode; + + private List sources; + + + public String getTenantCode() { + return tenantCode; + } + + public void setTenantCode(String tenantCode) { + this.tenantCode = tenantCode; + } + + public List getSources() { + return sources; + } + + public void setSources(List sources) { + this.sources = sources; + } + } + + @Autowired + public EvaluatorSourcesCacheService(EvaluatorSourcesCacheOptions options) { + super(options); + } + + @Override + protected Class valueClass() { + return EvaluatorSourceCacheValue.class; + } + + @Override + public String keyOf(EvaluatorSourceCacheValue value) { + return this.buildKey(value.getTenantCode()); + } + + public String buildKey(String tenantCod) { + HashMap keyParts = new HashMap<>(); + keyParts.put("$tenantCode$", tenantCod); + return this.generateKey(keyParts); + } +} diff --git a/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java index 5c81519f6..d9054cc21 100644 --- a/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/filetransformer/FileTransformerServiceImpl.java @@ -151,8 +151,6 @@ public class FileTransformerServiceImpl implements FileTransformerService { return null; } - - private List getFileTransformerSources() throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { String tenantCode = this.tenantScope.isSet() && this.tenantScope.isMultitenant() ? this.tenantScope.getTenantCode() : ""; FileTransformerSourcesCacheService.FileTransformerSourceCacheValue cacheValue = this.fileTransformerSourcesCacheService.lookup(this.fileTransformerSourcesCacheService.buildKey(tenantCode)); diff --git a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java index 66c3b9311..d39b3db91 100644 --- a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java @@ -869,8 +869,6 @@ public class PlanServiceImpl implements PlanService { } - - private void updateVersionStatusAndSave(PlanEntity data, PlanStatus previousStatus, PlanStatus newStatus) throws InvalidApplicationException { if (previousStatus == null && newStatus == null) return; diff --git a/backend/web/src/main/java/org/opencdmp/controllers/EvaluatorController.java b/backend/web/src/main/java/org/opencdmp/controllers/EvaluatorController.java new file mode 100644 index 000000000..b4e218890 --- /dev/null +++ b/backend/web/src/main/java/org/opencdmp/controllers/EvaluatorController.java @@ -0,0 +1,114 @@ +package org.opencdmp.controllers; + +import gr.cite.tools.auditing.AuditService; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.opencdmp.audit.AuditableAction; +import org.opencdmp.controllers.swagger.SwaggerHelpers; +import org.opencdmp.controllers.swagger.annotation.OperationWithTenantHeader; +import org.opencdmp.controllers.swagger.annotation.Swagger404; +import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; +import org.opencdmp.evaluatorbase.interfaces.EvaluatorConfiguration; +import org.opencdmp.evaluatorbase.models.misc.RankModel; +import org.opencdmp.model.evaluator.EvaluateRequestModel; +import org.opencdmp.model.file.ExportRequestModel; +import org.opencdmp.service.evaluator.EvaluatorService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.management.InvalidApplicationException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; + +@RestController +@CrossOrigin +@RequestMapping(value = "/api/evaluator") +@SwaggerCommonErrorResponses +public class EvaluatorController { + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(EvaluatorController.class)); + + private final EvaluatorService evaluatorService; + + private final AuditService auditService; + + @Autowired + public EvaluatorController(EvaluatorService evaluatorService, AuditService auditService){ + this.evaluatorService = evaluatorService; + this.auditService = auditService; + } + + @GetMapping("/available") + @OperationWithTenantHeader(summary = "Fetch all evaluators", description = SwaggerHelpers.Evaluator.endpoint_get_available_evaluators, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = EvaluatorConfiguration.class + ))) + )) + public List getAvailableConfigurations() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, InvalidApplicationException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + logger.debug(new MapLogEntry("getAvailableConfigurations")); + + List model = this.evaluatorService.getAvailableEvaluators(); + this.auditService.track(AuditableAction.Evaluator_GetAvailableConfigurations); + + return model; + } + + @PostMapping("/rank-plan") + @OperationWithTenantHeader(summary = "Rank a plan", description = SwaggerHelpers.Evaluator.endpoint_rank_plans, + responses = @ApiResponse(description = "OK", responseCode = "200")) + public ResponseEntity rankPlan(@RequestBody EvaluateRequestModel requestModel) throws Exception { + logger.debug(new MapLogEntry("ranking plan")); + + RankModel rankModel = this.evaluatorService.rankPlan(requestModel.getId(), requestModel.getEvaluatorId(), requestModel.getFormat(), true); + + return new ResponseEntity<>(rankModel, HttpStatus.OK); + } + + @PostMapping("/rank-description") + @OperationWithTenantHeader(summary = "Rank a description", description = SwaggerHelpers.Evaluator.endpoint_rank_descriptions, + responses = @ApiResponse(description = "OK", responseCode = "200")) + public ResponseEntity rankDescription(@RequestBody EvaluateRequestModel requestModel) throws Exception { + logger.debug(new MapLogEntry("ranking description")); + + RankModel rankModel = this.evaluatorService.rankDescription(requestModel.getId(), requestModel.getEvaluatorId(), requestModel.getFormat(), true); + + return new ResponseEntity<>(rankModel, HttpStatus.OK); + } + + @GetMapping("/{evaluatorId}/logo") + @OperationWithTenantHeader(summary = "Fetch a specific evaluator logo by id", description = SwaggerHelpers.Deposit.endpoint_get_logo, + responses = @ApiResponse(description = "OK", responseCode = "200", content = @Content( + schema = @Schema( + implementation = String.class + )) + )) + @Swagger404 + public String getLogo( + @Parameter(name = "evaluatorId", description = "The id of an evaluator of which to fetch the logo", example = "zenodo", required = true) @PathVariable("evaluatorId") String evaluatorId + ) throws InvalidApplicationException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + logger.debug(new MapLogEntry("get logo" + EvaluatorConfiguration.class.getSimpleName()).And("evaluatorId", evaluatorId)); + + String logo = this.evaluatorService.getLogo(evaluatorId); + this.auditService.track(AuditableAction.Deposit_GetLogo, Map.ofEntries( + new AbstractMap.SimpleEntry("evaluatorId", evaluatorId) + )); + + return logo; + } +} diff --git a/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java b/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java index a01110aaf..db2bdc656 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/swagger/SwaggerHelpers.java @@ -2589,6 +2589,24 @@ public final class SwaggerHelpers { } + public static final class Evaluator { + + public static final String endpoint_get_available_evaluators = + """ + This endpoint is used to fetch all the available evaluators. + """; + + public static final String endpoint_rank_plans = + """ + This endpoint is used to rank a plan using a specific evaluator. + """; + + public static final String endpoint_rank_descriptions = + """ + This endpoint is used to rank a description using a specific evaluator. + """; + } + public static final class EntityDoi { public static final String endpoint_query = diff --git a/backend/web/src/main/resources/config/application.yml b/backend/web/src/main/resources/config/application.yml index 89759d1b5..830787ed4 100644 --- a/backend/web/src/main/resources/config/application.yml +++ b/backend/web/src/main/resources/config/application.yml @@ -29,6 +29,7 @@ spring: optional:classpath:config/public-api.yml[.yml], optional:classpath:config/public-api-${spring.profiles.active}.yml[.yml], optional:file:../config/public-api-${spring.profiles.active}.yml[.yml], optional:classpath:config/dashboard.yml[.yml], optional:classpath:config/dashboard-${spring.profiles.active}.yml[.yml], optional:file:../config/dashboard-${spring.profiles.active}.yml[.yml], optional:classpath:config/file-transformer.yml[.yml], optional:classpath:config/file-transformer-${spring.profiles.active}.yml[.yml], optional:file:../config/file-transformer-${spring.profiles.active}.yml[.yml], + optional:classpath:config/evaluator.yml[.yml], optional:classpath:config/evaluator-${spring.profiles.active}.yml[.yml], optional:file:../config/evaluator-${spring.profiles.active}.yml[.yml], optional:classpath:config/authorization.yml[.yml], optional:classpath:config/authorization-${spring.profiles.active}.yml[.yml], optional:file:../config/authorization-${spring.profiles.active}.yml[.yml], optional:classpath:config/metrics.yml[.yml], optional:classpath:config/metrics-${spring.profiles.active}.yml[.yml], optional:file:../config/metrics-${spring.profiles.active}.yml[.yml], optional:classpath:config/field-set-expander.yml[.yml], optional:classpath:config/field-set-expander-${spring.profiles.active}.yml[.yml], optional:file:../config/field-set-expander-${spring.profiles.active}.yml[.yml], diff --git a/backend/web/src/main/resources/config/cache.yml b/backend/web/src/main/resources/config/cache.yml index 1662b2e82..d81273941 100644 --- a/backend/web/src/main/resources/config/cache.yml +++ b/backend/web/src/main/resources/config/cache.yml @@ -50,12 +50,24 @@ cache: maximumSize: 500 enableRecordStats: false expireAfterWriteSeconds: 600 + - names: [ "evaluatorConfigById" ] + allowNullValues: true + initialCapacity: 100 + maximumSize: 500 + enableRecordStats: false + expireAfterWriteSeconds: 600 - names: [ "fileTransformerSourcesByTenant" ] allowNullValues: true initialCapacity: 100 maximumSize: 500 enableRecordStats: false expireAfterWriteSeconds: 600 + - names: [ "evaluatorSourcesByTenant" ] + allowNullValues: true + initialCapacity: 100 + maximumSize: 500 + enableRecordStats: false + expireAfterWriteSeconds: 600 - names: [ "tokenExchangeKey" ] allowNullValues: true initialCapacity: 100 @@ -126,9 +138,15 @@ cache: fileTransformerConfigById: name: fileTransformerConfigById keyPattern: file_transformer_config_by_id_$transformerId$_$tenantCode$:v0 + evaluatorConfigById: + name: evaluatorConfigById + keyPattern: evaluator_config_by_id_$evaluatorId$_$tenantCode$:v0 fileTransformerSourcesByTenant: name: fileTransformerSourcesByTenant keyPattern: ile_transformer_sources_by_tenant_$tenantCode$:v0 + evaluatorSourcesByTenant: + name: evaluatorSourcesByTenant + keyPattern: evaluator_sources_by_tenant_$tenantCode$:v0 token-exchange-key: name: tokenExchangeKey keyPattern: resolve_$keyhash$:v0 diff --git a/backend/web/src/main/resources/config/evaluator-devel.yml b/backend/web/src/main/resources/config/evaluator-devel.yml new file mode 100644 index 000000000..d16babf20 --- /dev/null +++ b/backend/web/src/main/resources/config/evaluator-devel.yml @@ -0,0 +1,9 @@ +evaluator: + sources: + - url: http://localhost:8084 + evaluatorId: fair + issuer-url: ${IDP_ISSUER_URI_TOKEN} + client-id: ${IDP_APIKEY_CLIENT_ID} + client-secret: ${IDP_APIKEY_CLIENT_SECRET} + scope: ${IDP_APIKEY_SCOPE} + maxInMemorySizeInBytes: 6554000 \ No newline at end of file diff --git a/backend/web/src/main/resources/config/evaluator.yml b/backend/web/src/main/resources/config/evaluator.yml new file mode 100644 index 000000000..24b2af433 --- /dev/null +++ b/backend/web/src/main/resources/config/evaluator.yml @@ -0,0 +1,2 @@ +evaluator: + sources: [] \ No newline at end of file diff --git a/backend/web/src/main/resources/config/permissions.yml b/backend/web/src/main/resources/config/permissions.yml index a53416882..2805ea3ad 100644 --- a/backend/web/src/main/resources/config/permissions.yml +++ b/backend/web/src/main/resources/config/permissions.yml @@ -371,6 +371,17 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + EvaluateDescription: + roles: + - Admin + - TenantAdmin + plan: + roles: + - Owner + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false CloneDescription: roles: - Admin @@ -658,6 +669,17 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + EvaluatePlan: + roles: + - Admin + - TenantAdmin + plan: + roles: + - Owner + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false ClonePlan: roles: - Admin diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 90ff854e4..f0e9c8a22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,8 +28,8 @@ "cookieconsent": "^3.1.1", "dragula": "^3.7.3", "file-saver": "^2.0.5", - "keycloak-angular": "^15.2.1", - "keycloak-js": "^24.0.5", + "keycloak-angular": "^16.0.1", + "keycloak-js": "^25.0.0", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "ng-dialog-animation": "^9.0.4", @@ -8856,23 +8856,23 @@ } }, "node_modules/keycloak-angular": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-15.2.1.tgz", - "integrity": "sha512-7w8bkJQ9OBtBJt5eNfqnRG2IL9btvp8Stf2fpVipSE1C/qtd5UQ31skx735PMPgMTUFsdz/0VA32Gmsng54+Xg==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-16.0.1.tgz", + "integrity": "sha512-ytkL32R/tfHEyZ3txQtgH1y0WofW/D36zTbo2agDCYUtZETq0wAQ3E/4bVDUAr6ZKwotgAnIyOORfErnvDkXng==", "dependencies": { "tslib": "^2.3.1" }, "peerDependencies": { - "@angular/common": "^17", - "@angular/core": "^17", - "@angular/router": "^17", - "keycloak-js": "^18 || ^19 || ^20 || ^21 || ^22 || ^23 || ^24" + "@angular/common": "^18", + "@angular/core": "^18", + "@angular/router": "^18", + "keycloak-js": "^18 || ^19 || ^20 || ^21 || ^22 || ^23 || ^24 || ^25" } }, "node_modules/keycloak-js": { - "version": "24.0.5", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-24.0.5.tgz", - "integrity": "sha512-VQOSn3j13DPB6OuavKAq+sRjDERhIKrXgBzekoHRstifPuyULILguugX6yxRUYFSpn3OMYUXmSX++tkdCupOjA==", + "version": "25.0.6", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.6.tgz", + "integrity": "sha512-Km+dc+XfNvY6a4az5jcxTK0zPk52ns9mAxLrHj7lF3V+riVYvQujfHmhayltJDjEpSOJ4C8a57LFNNKnNnRP2g==", "dependencies": { "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0" diff --git a/frontend/src/app/core/common/enum/evaluator-entity-type.ts b/frontend/src/app/core/common/enum/evaluator-entity-type.ts new file mode 100644 index 000000000..911900bfa --- /dev/null +++ b/frontend/src/app/core/common/enum/evaluator-entity-type.ts @@ -0,0 +1,4 @@ +export enum EvaluatorEntityType { + Plan = 0, + Description = 1 +} \ No newline at end of file diff --git a/frontend/src/app/core/common/enum/permission.enum.ts b/frontend/src/app/core/common/enum/permission.enum.ts index dfbf8d2a0..69549d222 100644 --- a/frontend/src/app/core/common/enum/permission.enum.ts +++ b/frontend/src/app/core/common/enum/permission.enum.ts @@ -84,6 +84,7 @@ export enum AppPermission { AssignPlanUsers = "AssignPlanUsers", InvitePlanUsers = "InvitePlanUsers", AnnotatePlan = "AnnotatePlan", + EvaluatePlan = "EvaluatePlan", //PlanStatus BrowsePlanStatus = "BrowsePlanStatus", @@ -121,6 +122,7 @@ export enum AppPermission { DeleteDescription = "DeleteDescription", CloneDescription = "CloneDescription", ExportDescription = "ExportDescription", + EvaluateDescription = "EvaluateDescription", //DescriptionTag BrowseDescriptionTag = "BrowseDescriptionTag", diff --git a/frontend/src/app/core/core-service.module.ts b/frontend/src/app/core/core-service.module.ts index 14fbaeff1..d9481b26f 100644 --- a/frontend/src/app/core/core-service.module.ts +++ b/frontend/src/app/core/core-service.module.ts @@ -52,6 +52,7 @@ import { PlanStatusService } from './services/plan/plan-status.service'; import { DescriptionStatusService } from './services/description-status/description-status.service'; import { PlanWorkflowService } from './services/plan/plan-workflow.service'; import { DescriptionWorkflowService } from './services/description-workflow/description-workflow.service'; +import { EvaluatorHttpService } from './services/evaluator/evaluator.http.service'; // // // This is shared module that provides all the services. Its imported only once on the AppModule. @@ -112,6 +113,7 @@ export class CoreServiceModule { CanDeactivateGuard, FileTransformerService, FileTransformerHttpService, + EvaluatorHttpService, SemanticsService, PrefillingSourceService, VisibilityRulesService, diff --git a/frontend/src/app/core/model/evaluator/evaluator-configuration.ts b/frontend/src/app/core/model/evaluator/evaluator-configuration.ts new file mode 100644 index 000000000..873c636ed --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-configuration.ts @@ -0,0 +1,11 @@ +import { RankType } from "./rank-type"; +import { EvaluatorEntityType } from "@app/core/common/enum/evaluator-entity-type"; +import { RankConfig } from "./rank-config"; + +export class EvaluatorConfiguration{ + evaluatorId: string; + rankType: RankType[]; + evaluatorEntityTypes: EvaluatorEntityType[]; + rankConfig: RankConfig[]; + hasLogo: boolean; +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-format.model.ts b/frontend/src/app/core/model/evaluator/evaluator-format.model.ts new file mode 100644 index 000000000..f31cc8675 --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-format.model.ts @@ -0,0 +1,12 @@ +import { EvaluatorEntityType } from "@app/core/common/enum/evaluator-entity-type"; +import { RankType } from "./rank-type"; +import { SelectionConfiguration } from "./evaluator-selection"; +import { ValueRangeConfiguration } from "./evaluator-value-range"; + +export interface EvaluatorFormat { + rankType: RankType[]; + selectionConfiguration: SelectionConfiguration; + valueRangeConfiguration: ValueRangeConfiguration; + evaluatorId: string; + entityTypes: EvaluatorEntityType[]; +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-number-type.model.ts b/frontend/src/app/core/model/evaluator/evaluator-number-type.model.ts new file mode 100644 index 000000000..fb0f1addd --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-number-type.model.ts @@ -0,0 +1,4 @@ +export enum NumberType { + Decimal = 0, + Integer = 1 +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-plan-model.model.ts b/frontend/src/app/core/model/evaluator/evaluator-plan-model.model.ts new file mode 100644 index 000000000..986c377db --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-plan-model.model.ts @@ -0,0 +1,5 @@ +export class RankModel { + rank: number; + details: string; + messages: { [key: string]: string }; +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-selection.ts b/frontend/src/app/core/model/evaluator/evaluator-selection.ts new file mode 100644 index 000000000..367c82c86 --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-selection.ts @@ -0,0 +1,9 @@ +import { ValueSet } from "./evaluator-value-set"; + +export class SelectionConfiguration { + valueSetList: ValueSet[]; + + constructor(valueSetList: ValueSet[]) { + this.valueSetList = valueSetList; + } +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-success-status.model.ts b/frontend/src/app/core/model/evaluator/evaluator-success-status.model.ts new file mode 100644 index 000000000..2405ff26d --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-success-status.model.ts @@ -0,0 +1,4 @@ +export enum SuccessStatus { + Fail = 0, + Pass = 1 +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-value-range.ts b/frontend/src/app/core/model/evaluator/evaluator-value-range.ts new file mode 100644 index 000000000..d1b8ba304 --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-value-range.ts @@ -0,0 +1,15 @@ +import { NumberType } from "./evaluator-number-type.model"; + +export class ValueRangeConfiguration { + numberType: NumberType; + min: number; + max: number; + minPassValue: number; + + constructor(numberType: NumberType, min: number, max: number, minPassValue: number) { + this.numberType = numberType; + this.min = min; + this.max = max; + this.minPassValue = minPassValue; + } +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/evaluator-value-set.ts b/frontend/src/app/core/model/evaluator/evaluator-value-set.ts new file mode 100644 index 000000000..390e9fd6d --- /dev/null +++ b/frontend/src/app/core/model/evaluator/evaluator-value-set.ts @@ -0,0 +1,11 @@ +import { SuccessStatus } from "./evaluator-success-status.model"; + +export class ValueSet { + key: number; + successStatus: SuccessStatus; + + constructor(key: number, successStatus: SuccessStatus) { + this.key = key; + this.successStatus = successStatus; + } +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/rank-config.ts b/frontend/src/app/core/model/evaluator/rank-config.ts new file mode 100644 index 000000000..d79792116 --- /dev/null +++ b/frontend/src/app/core/model/evaluator/rank-config.ts @@ -0,0 +1,9 @@ +import { RankType } from "./rank-type"; +import { ValueRangeConfiguration } from "./evaluator-value-range"; +import { SelectionConfiguration } from "./evaluator-selection"; + +export class RankConfig{ + rankType: RankType; + valueRangeConfiguration?: ValueRangeConfiguration; + selectionConfiguration?: SelectionConfiguration; +} \ No newline at end of file diff --git a/frontend/src/app/core/model/evaluator/rank-type.ts b/frontend/src/app/core/model/evaluator/rank-type.ts new file mode 100644 index 000000000..daef9a77f --- /dev/null +++ b/frontend/src/app/core/model/evaluator/rank-type.ts @@ -0,0 +1,4 @@ +export enum RankType { + ValueRange = 0, + Selection = 1 +} \ No newline at end of file diff --git a/frontend/src/app/core/services/evaluator/evaluator.http.service.ts b/frontend/src/app/core/services/evaluator/evaluator.http.service.ts new file mode 100644 index 000000000..30a305180 --- /dev/null +++ b/frontend/src/app/core/services/evaluator/evaluator.http.service.ts @@ -0,0 +1,44 @@ +import { HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { BaseService } from '@common/base/base.service'; +import { Guid } from '@common/types/guid'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { ConfigurationService } from '../configuration/configuration.service'; +import { BaseHttpV2Service } from '../http/base-http-v2.service'; +import { EvaluatorFormat } from '@app/core/model/evaluator/evaluator-format.model'; +import { RankModel } from '@app/core/model/evaluator/evaluator-plan-model.model'; + + +@Injectable() +export class EvaluatorHttpService extends BaseService { + + private headers = new HttpHeaders(); + + constructor( + private http: BaseHttpV2Service, + private configurationService: ConfigurationService + ) { super(); } + + private get apiBase(): string { return `${this.configurationService.server}evaluator`; } + + getAvailableConfigurations(): Observable { + const url = `${this.apiBase}/available`; + return this.http.get(url).pipe(catchError((error: any) => throwError(error))); + } + + rankPlan(id: Guid, evaluatorId: string, format: string): Observable { + const url = `${this.apiBase}/rank-plan`; + return this.http.post(url, {id: id, evaluatorId: evaluatorId, format: format}, {responseType: 'json', observe: 'response'}).pipe(catchError((error: any) => throwError(error))); + } + + rankDescription(id: Guid, evaluatorId: string, format: string): Observable { + const url = `${this.apiBase}/rank-description`; + return this.http.post(url, {id: id, evaluatorId: evaluatorId, format: format}, {responseType: 'json', observe: 'response'}).pipe(catchError((error: any) => throwError(error))); + } + + getLogo(evaluatorId: string): Observable{ + const url = `${this.apiBase}/${evaluatorId}/logo`; + return this.http.get(url).pipe(catchError((error: any) => throwError(error))); + } +} \ No newline at end of file diff --git a/frontend/src/app/core/services/evaluator/evaluator.service.ts b/frontend/src/app/core/services/evaluator/evaluator.service.ts new file mode 100644 index 000000000..a276331ff --- /dev/null +++ b/frontend/src/app/core/services/evaluator/evaluator.service.ts @@ -0,0 +1,136 @@ +import { Component, EventEmitter, Input, OnInit, Output, Injectable } from '@angular/core'; +import { BaseService } from '@common/base/base.service'; +import { catchError, takeUntil } from 'rxjs/operators'; +import { EvaluatorHttpService } from './evaluator.http.service'; +import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { AuthService } from '../auth/auth.service'; +import { EvaluatorEntityType } from '@app/core/common/enum/evaluator-entity-type'; +import { EvaluatorConfiguration } from '@app/core/model/evaluator/evaluator-configuration'; +import { TranslateService } from '@ngx-translate/core'; +import { Guid } from '@common/types/guid'; +import { + SnackBarNotificationLevel, + UiNotificationService +} from '@app/core/services/notification/ui-notification-service'; +import { Observable, throwError } from 'rxjs'; +import { tap, share } from 'rxjs/operators'; +import { RankModel } from '@app/core/model/evaluator/evaluator-plan-model.model'; + + +@Injectable({ + providedIn: 'root' +}) +export class EvaluatorService extends BaseService { + + constructor( + private evaluatorHttpService: EvaluatorHttpService, + private authentication: AuthService, + private httpErrorHandlingService: HttpErrorHandlingService, + private language: TranslateService, + private uiNotificationService: UiNotificationService, + ) { super(); } + + private _initialized: boolean = false; + private _loading: boolean = false; + + private _availableEvaluators: EvaluatorConfiguration[] = []; + + get availableEvaluators(): EvaluatorConfiguration[] { + if (!this.authentication.currentAccountIsAuthenticated()) { + return []; + } + if (!this._initialized && !this._loading) this.init(); // if not initialized and loading calls init to initialize the evaluators. + return this._availableEvaluators; + } + + public availableEvaluatorsFor(entityType: EvaluatorEntityType) { + // Filter evaluators by entity type + // The fetch logo config should be here. + if (this.availableEvaluators) { + const filteredEvaluators = this.availableEvaluators.filter(x => { + return x.evaluatorEntityTypes && x.evaluatorEntityTypes.includes(entityType); + }); + + return filteredEvaluators; + } + + return []; + } + + init() { + this._loading = true; + this.evaluatorHttpService.getAvailableConfigurations() + .pipe(takeUntil(this._destroyed), catchError((error) => { + this._loading = false; + this._initialized = true; + this.httpErrorHandlingService.handleBackedRequestError(error); + return []; + })) + .subscribe(items => { + this._availableEvaluators = items; + this._loading = false; + this._initialized = true; + }); + } + + rankPlan(id: Guid, evaluatorId: string, format: string, isPublic: boolean = false): Observable { + this._loading = true; + + return this.evaluatorHttpService.rankPlan(id, evaluatorId, format).pipe( + tap({ + next: (doi) => { + this.onCallbackSuccess(); + }, + error: (error) => { + this.onCallbackError(error); + // Ensure loading state is turned off in case of error + this._loading = false; + }, + complete: () => { + this._loading = false; + } + }), + catchError((error) => { + // Ensure loading state is turned off in case of error + this._loading = false; + return throwError(error); + }), + share() + ); + } + + rankDescription(id: Guid, evaluatorId: string, format: string, isPublic: boolean = false): Observable { + this._loading = true; + return this.evaluatorHttpService.rankDescription(id, evaluatorId, format) + .pipe( + takeUntil(this._destroyed), + tap(response => { + this._loading = false; + this.onCallbackSuccess(); + }), + catchError(error => { + this._loading = false; + this.onCallbackError(error); + return throwError(error); + }) + ); + } + + getLogo(evaluatorId: string): Observable { + return this.evaluatorHttpService.getLogo(evaluatorId).pipe( + catchError((error) => { + this.httpErrorHandlingService.handleBackedRequestError(error); + return throwError(error); + }) + ); + } + + onCallbackSuccess(): void { + this.uiNotificationService.snackBarNotification(this.language.instant('PLAN-EDITOR.SNACK-BAR.SUCCESSFUL-EVALUATION'), SnackBarNotificationLevel.Success); + } + + onCallbackError(error) { + this.uiNotificationService.snackBarNotification(error.error.message ? error.error.message : this.language.instant('PLAN-EDITOR.SNACK-BAR.UNSUCCESSFUL-EVALUATION'), SnackBarNotificationLevel.Error); + } + +} \ No newline at end of file diff --git a/frontend/src/app/ui/description/description.module.ts b/frontend/src/app/ui/description/description.module.ts index c02df870b..9e6268ccb 100644 --- a/frontend/src/app/ui/description/description.module.ts +++ b/frontend/src/app/ui/description/description.module.ts @@ -3,6 +3,7 @@ import { FormattingModule } from '@app/core/formatting.module'; import { DescriptionRoutingModule, PublicDescriptionRoutingModule } from '@app/ui/description/description.routing'; import { CommonFormsModule } from '@common/forms/common-forms.module'; import { CommonUiModule } from '@common/ui/common-ui.module'; +import { EvaluateDescriptionDialogModule } from './evaluate-description-dialog/evaluate-description-dialog.module'; @NgModule({ imports: [ @@ -24,6 +25,7 @@ export class DescriptionModule { } CommonFormsModule, FormattingModule, PublicDescriptionRoutingModule, + EvaluateDescriptionDialogModule, ], declarations: [ ], diff --git a/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.html b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.html new file mode 100644 index 000000000..428c53273 --- /dev/null +++ b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.html @@ -0,0 +1,35 @@ +{{ 'DESCRIPTION-EVALUATE-DIALOG.HEADER' | translate }} + + + + {{'DESCRIPTION-EVALUATE-DIALOG.DETAILS-SUB-HEADER' | translate}} + + + + + {{'DESCRIPTION-EVALUATE-DIALOG.RANK-BODY' | translate}} + + + + {{'DESCRIPTION-EVALUATE-DIALOG.DETAILS-BODY' | translate}} + + + + + + {{'DESCRIPTION-EVALUATE-DIALOG.MESSAGES-BODY' | translate}} + + + + + + {{ entry.key }}: {{ entry.value }} + + + + + + + + + diff --git a/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.scss b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.scss new file mode 100644 index 000000000..b44d417e6 --- /dev/null +++ b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.scss @@ -0,0 +1,8 @@ +.dialog-content { + display: flex; + flex-direction: column; /* Stack form fields vertically */ + } + + mat-form-field { + margin-bottom: 16px; /* Space between fields */ + } \ No newline at end of file diff --git a/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.spec.ts b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.spec.ts new file mode 100644 index 000000000..da4d38c7f --- /dev/null +++ b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EvaluateDescriptionDialogComponent } from './evaluate-description-dialog.component'; + +describe('EvaluateDescriptionDialogComponent', () => { + let component: EvaluateDescriptionDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EvaluateDescriptionDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EvaluateDescriptionDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.ts b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.ts new file mode 100644 index 000000000..7d91672b1 --- /dev/null +++ b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { RankModel } from '@app/core/model/evaluator/evaluator-plan-model.model'; + +@Component({ + selector: 'app-evaluate-description-dialog', + templateUrl: './evaluate-description-dialog.component.html', + styleUrl: './evaluate-description-dialog.component.scss' +}) +export class EvaluateDescriptionDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { rankData: RankModel } + ) { } +} + diff --git a/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.module.ts b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.module.ts new file mode 100644 index 000000000..200182ef6 --- /dev/null +++ b/frontend/src/app/ui/description/evaluate-description-dialog/evaluate-description-dialog.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { NgIf } from '@angular/common'; +import { EvaluateDescriptionDialogComponent } from './evaluate-description-dialog.component'; +import { CommonUiModule } from '@common/ui/common-ui.module'; + +@NgModule({ + imports: [ + CommonModule, + MatCardModule, + MatButtonModule, + MatInputModule, + MatFormFieldModule, + NgIf, + CommonUiModule + ], + declarations: [EvaluateDescriptionDialogComponent], + exports: [EvaluateDescriptionDialogComponent] +}) +export class EvaluateDescriptionDialogModule { } \ No newline at end of file diff --git a/frontend/src/app/ui/description/overview/description-overview.component.html b/frontend/src/app/ui/description/overview/description-overview.component.html index 931ba9d43..9771fe99d 100644 --- a/frontend/src/app/ui/description/overview/description-overview.component.html +++ b/frontend/src/app/ui/description/overview/description-overview.component.html @@ -168,6 +168,28 @@ + 0"> + + + + open_in_new + + + + {{ 'DESCRIPTION-OVERVIEW.ACTIONS.EVALUATE' | translate }} + + + + + + + {{ (evaluator.evaluatorId?.toUpperCase()) | translate }} + + + + + diff --git a/frontend/src/app/ui/description/overview/description-overview.component.scss b/frontend/src/app/ui/description/overview/description-overview.component.scss index 229103750..c6f04e8c9 100644 --- a/frontend/src/app/ui/description/overview/description-overview.component.scss +++ b/frontend/src/app/ui/description/overview/description-overview.component.scss @@ -273,4 +273,9 @@ } .deleted-item { color: #cf1407; +} +.logo { + margin-right: 16px; + max-width: 24px; + max-height: 24px; } \ No newline at end of file diff --git a/frontend/src/app/ui/description/overview/description-overview.component.ts b/frontend/src/app/ui/description/overview/description-overview.component.ts index 7c2fc3b52..25324085d 100644 --- a/frontend/src/app/ui/description/overview/description-overview.component.ts +++ b/frontend/src/app/ui/description/overview/description-overview.component.ts @@ -2,17 +2,24 @@ import { Location } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { DescriptionStatusEnum } from '@app/core/common/enum/description-status'; -import { PlanAccessType } from '@app/core/common/enum/plan-access-type'; -import { PlanStatusEnum } from '@app/core/common/enum/plan-status'; -import { PlanUserRole } from '@app/core/common/enum/plan-user-role'; +import { DescriptionStatusAvailableActionType } from '@app/core/common/enum/description-status-available-action-type'; +import { DescriptionStatusPermission } from '@app/core/common/enum/description-status-permission.enum'; +import { EvaluatorEntityType } from '@app/core/common/enum/evaluator-entity-type'; import { FileTransformerEntityType } from '@app/core/common/enum/file-transformer-entity-type'; import { IsActive } from '@app/core/common/enum/is-active.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { PlanAccessType } from '@app/core/common/enum/plan-access-type'; +import { PlanStatusEnum } from '@app/core/common/enum/plan-status'; +import { PlanUserRole } from '@app/core/common/enum/plan-user-role'; +import { DescriptionStatus, DescriptionStatusDefinition } from '@app/core/model/description-status/description-status'; import { DescriptionTemplate } from '@app/core/model/description-template/description-template'; import { BaseDescription, Description, DescriptionStatusPersist, PublicDescription } from '@app/core/model/description/description'; +import { RankModel } from '@app/core/model/evaluator/evaluator-plan-model.model'; import { PlanBlueprint, PlanBlueprintDefinition, PlanBlueprintDefinitionSection } from '@app/core/model/plan-blueprint/plan-blueprint'; +import { PlanStatus } from '@app/core/model/plan-status/plan-status'; import { Plan, PlanDescriptionTemplate, PlanUser, PlanUserRemovePersist } from '@app/core/model/plan/plan'; import { PlanReference } from '@app/core/model/plan/plan-reference'; import { ReferenceType } from '@app/core/model/reference-type/reference-type'; @@ -21,20 +28,23 @@ import { User } from '@app/core/model/user/user'; import { AuthService } from '@app/core/services/auth/auth.service'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { DescriptionService } from '@app/core/services/description/description.service'; -import { PlanService } from '@app/core/services/plan/plan.service'; +import { EvaluatorService } from '@app/core/services/evaluator/evaluator.service'; import { FileTransformerService } from '@app/core/services/file-transformer/file-transformer.service'; import { LockService } from '@app/core/services/lock/lock.service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; import { AnalyticsService } from '@app/core/services/matomo/analytics-service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { PlanService } from '@app/core/services/plan/plan.service'; import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service'; import { ReferenceService } from '@app/core/services/reference/reference.service'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; import { UserService } from '@app/core/services/user/user.service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; import { FileUtils } from '@app/core/services/utilities/file-utils.service'; import { PopupNotificationDialogComponent } from '@app/library/notification/popup/popup-notification.component'; -import { DescriptionValidationOutput } from '@app/ui/plan/plan-finalize-dialog/plan-finalize-dialog.component'; -import { PlanInvitationDialogComponent } from '@app/ui/plan/invitation/dialog/plan-invitation-dialog.component'; import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; +import { PlanInvitationDialogComponent } from '@app/ui/plan/invitation/dialog/plan-invitation-dialog.component'; +import { DescriptionValidationOutput } from '@app/ui/plan/plan-finalize-dialog/plan-finalize-dialog.component'; import { BaseComponent } from '@common/base/base.component'; import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; @@ -43,11 +53,7 @@ import { TranslateService } from '@ngx-translate/core'; import { map, takeUntil } from 'rxjs/operators'; import { nameof } from 'ts-simple-nameof'; import { DescriptionCopyDialogComponent } from '../description-copy-dialog/description-copy-dialog.component'; -import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; -import { DescriptionStatus, DescriptionStatusDefinition } from '@app/core/model/description-status/description-status'; -import { PlanStatus } from '@app/core/model/plan-status/plan-status'; -import { DescriptionStatusAvailableActionType } from '@app/core/common/enum/description-status-available-action-type'; -import { DescriptionStatusPermission } from '@app/core/common/enum/description-status-permission.enum'; +import { EvaluateDescriptionDialogComponent } from './../evaluate-description-dialog/evaluate-description-dialog.component'; @Component({ @@ -70,6 +76,8 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni planStatusEnum = PlanStatusEnum; planUserRoleEnum = PlanUserRole; fileTransformerEntityTypeEnum = FileTransformerEntityType; + evaluatorEntityTypeEnum = EvaluatorEntityType; + logos: Map = new Map(); canEdit = false; canCopy = false; @@ -77,10 +85,12 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni canFinalize = false; canAnnotate = false; canInvitePlanUsers = false; - get canAssignPlanUsers(): boolean { - const authorizationFlags = !this.isPublicView ? (this.description?.plan as Plan)?.authorizationFlags : []; - return (authorizationFlags?.some(x => x === AppPermission.InvitePlanUsers) || this.authentication.hasPermission(AppPermission.InvitePlanUsers)) && - !this.isPublicView && this.description?.belongsToCurrentTenant && this.isActive && (this.description?.plan?.status?.internalStatus == null || this.description?.plan?.status?.internalStatus != PlanStatusEnum.Finalized); + canEvaluate = false; + + get canAssignPlanUsers(): boolean { + const authorizationFlags = !this.isPublicView ? (this.description?.plan as Plan)?.authorizationFlags : []; + return (authorizationFlags?.some(x => x === AppPermission.InvitePlanUsers) || this.authentication.hasPermission(AppPermission.InvitePlanUsers)) && + !this.isPublicView && this.description?.belongsToCurrentTenant && this.isActive && (this.description?.plan?.status?.internalStatus == null || this.description?.plan?.status?.internalStatus != PlanStatusEnum.Finalized); } authorFocus: string; @@ -110,6 +120,9 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni private breadcrumbService: BreadcrumbService, private httpErrorHandlingService: HttpErrorHandlingService, private userService: UserService, + private evaluatorService: EvaluatorService, + private logger: LoggingService, + private sanitizer: DomSanitizer, ) { super(); } @@ -122,6 +135,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.canCopy = false; this.canFinalize = false; this.canInvitePlanUsers = false; + this.canEvaluate = false; // Gets description data using parameter id this.route.params .pipe(takeUntil(this._destroyed)) @@ -134,43 +148,46 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.descriptionService.getSingle(itemId, this.lookupFields()) .pipe(takeUntil(this._destroyed)) .subscribe( - { - next: (data) => { - this.breadcrumbService.addIdResolvedValue(data.id.toString(), data.label); + { + next: (data) => { + this.breadcrumbService.addIdResolvedValue(data.id.toString(), data.label); - this.description = data; - this.description.plan.planUsers = this.isActive || this.description.plan.isActive === IsActive.Active ? data.plan.planUsers.filter(x => x.isActive === IsActive.Active) : data.plan.planUsers; - this.researchers = this.referenceService.getReferencesForTypes(this.description?.plan?.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); - this.checkLockStatus(this.description.id); - this.canDelete = this.isActive && (this.authService.hasPermission(AppPermission.DeleteDescription) || - this.description.authorizationFlags?.some(x => x === AppPermission.DeleteDescription)) && this.description.belongsToCurrentTenant != false; + this.description = data; + this.description.plan.planUsers = this.isActive || this.description.plan.isActive === IsActive.Active ? data.plan.planUsers.filter(x => x.isActive === IsActive.Active) : data.plan.planUsers; + this.researchers = this.referenceService.getReferencesForTypes(this.description?.plan?.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); + this.checkLockStatus(this.description.id); + this.canDelete = this.isActive && (this.authService.hasPermission(AppPermission.DeleteDescription) || + this.description.authorizationFlags?.some(x => x === AppPermission.DeleteDescription)) && this.description.belongsToCurrentTenant != false; - this.canEdit = (this.authService.hasPermission(AppPermission.EditDescription) || - this.description.authorizationFlags?.some(x => x === AppPermission.EditDescription)) && this.description.belongsToCurrentTenant != false; - - this.canCopy = this.canEdit || (this.authService.hasPermission(AppPermission.PublicCloneDescription) && this.isPublicView); + this.canEdit = (this.authService.hasPermission(AppPermission.EditDescription) || + this.description.authorizationFlags?.some(x => x === AppPermission.EditDescription)) && this.description.belongsToCurrentTenant != false; - this.canAnnotate = (this.authService.hasPermission(AppPermission.AnnotateDescription) || - this.description.authorizationFlags?.some(x => x === AppPermission.AnnotateDescription)) && this.description.belongsToCurrentTenant != false; + this.canCopy = this.canEdit || (this.authService.hasPermission(AppPermission.PublicCloneDescription) && this.isPublicView); - this.canFinalize = this.isActive && (this.authService.hasPermission(AppPermission.FinalizeDescription) || - this.description.authorizationFlags?.some(x => x === AppPermission.FinalizeDescription)) && this.description.belongsToCurrentTenant != false; + this.canAnnotate = (this.authService.hasPermission(AppPermission.AnnotateDescription) || + this.description.authorizationFlags?.some(x => x === AppPermission.AnnotateDescription)) && this.description.belongsToCurrentTenant != false; - this.canInvitePlanUsers = this.isActive && (this.authService.hasPermission(AppPermission.InvitePlanUsers) || - this.description.authorizationFlags?.some(x => x === AppPermission.InvitePlanUsers)) && this.description.belongsToCurrentTenant != false; + this.canFinalize = this.isActive && (this.authService.hasPermission(AppPermission.FinalizeDescription) || + this.description.authorizationFlags?.some(x => x === AppPermission.FinalizeDescription)) && this.description.belongsToCurrentTenant != false; - }, - error: (error: any) => { - this.httpErrorHandlingService.handleBackedRequestError(error); + this.canInvitePlanUsers = this.isActive && (this.authService.hasPermission(AppPermission.InvitePlanUsers) || + this.description.authorizationFlags?.some(x => x === AppPermission.InvitePlanUsers)) && this.description.belongsToCurrentTenant != false; - if (error.status === 404) { + this.canEvaluate = this.isActive && (this.authService.hasPermission(AppPermission.EvaluateDescription) || + this.description.authorizationFlags?.some(x => x === AppPermission.EvaluateDescription)) && this.description.belongsToCurrentTenant != false; + + }, + error: (error: any) => { + this.httpErrorHandlingService.handleBackedRequestError(error); + + if (error.status === 404) { return this.onFetchingDeletedCallbackError('/descriptions/'); - } - if (error.status === 403) { + } + if (error.status === 403) { return this.onFetchingForbiddenCallbackError('/descriptions/'); + } } - } - }); + }); } else if (publicId != null) { this.isNew = false; @@ -179,26 +196,26 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.descriptionService.getPublicSingle(publicId, this.lookupFields()) .pipe(takeUntil(this._destroyed)) .subscribe({ - next: (data) => { - this.canCopy = this.authService.hasPermission(AppPermission.PublicCloneDescription) && this.isPublicView; - - this.breadcrumbService.addExcludedParam('public', true); - this.breadcrumbService.addIdResolvedValue(data.id.toString(), data.label); - - this.description = data; - this.researchers = this.referenceService.getReferencesForTypes(this.description?.plan?.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); - }, - error: (error: any) => { - this.httpErrorHandlingService.handleBackedRequestError(error); - - if (error.status === 404) { - return this.onFetchingDeletedCallbackError('/explore-descriptions'); - } - if (error.status === 403) { - return this.onFetchingForbiddenCallbackError('/explore-descriptions'); - } - } - }); + next: (data) => { + this.canCopy = this.authService.hasPermission(AppPermission.PublicCloneDescription) && this.isPublicView; + + this.breadcrumbService.addExcludedParam('public', true); + this.breadcrumbService.addIdResolvedValue(data.id.toString(), data.label); + + this.description = data; + this.researchers = this.referenceService.getReferencesForTypes(this.description?.plan?.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); + }, + error: (error: any) => { + this.httpErrorHandlingService.handleBackedRequestError(error); + + if (error.status === 404) { + return this.onFetchingDeletedCallbackError('/explore-descriptions'); + } + if (error.status === 403) { + return this.onFetchingForbiddenCallbackError('/explore-descriptions'); + } + } + }); } }); @@ -225,33 +242,62 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni return this.description?.status?.definition?.availableActions?.filter(x => x === DescriptionStatusAvailableActionType.Export).length > 0; } - get canEditStatus(): boolean{ - return (this.description as Description).statusAuthorizationFlags?.some(x => x.toLowerCase() === DescriptionStatusPermission.Edit.toLowerCase()) - } + get canEditStatus(): boolean { + return (this.description as Description).statusAuthorizationFlags?.some(x => x.toLowerCase() === DescriptionStatusPermission.Edit.toLowerCase()) + } hasAvailableFinalizeStatus() { return (this.description as Description).availableStatuses?.find(x => x.internalStatus === DescriptionStatusEnum.Finalized) != null; } + onEvaluateDescription(planId: Guid, evaluatorId: string, format: string, isPublicView: boolean) { + this.evaluatorService.rankDescription(planId, evaluatorId, format).subscribe( + (response: RankModel) => { + this.evaluatorService.getLogo(evaluatorId).subscribe( + (logo: string) => { + + this.logos.set(evaluatorId, this.sanitizer.bypassSecurityTrustResourceUrl('data:image/png;base64, ' + logo)); + + const dialogRef = this.dialog.open(EvaluateDescriptionDialogComponent, { + data: { + rankData: response, + } + }); + + dialogRef.afterClosed().subscribe(result => { + this.logger.debug("Dialog closed with result:", result); + }); + }, + error => { + this.logger.error("Error fetching evaluator logo:", error); + } + ); + }, + error => { + this.logger.error("Error ranking description:", error); + } + ); + } + checkLockStatus(id: Guid) { this.lockService.checkLockStatus(id).pipe(takeUntil(this._destroyed)) .subscribe({ - next: (lockStatus) => { - this.isLocked = lockStatus.status; - if (this.isLocked) { - this.dialog.open(PopupNotificationDialogComponent, { - data: { - title: this.language.instant('DESCRIPTION-OVERVIEW.LOCKED-DIALOG.TITLE'), - message: this.language.instant('DESCRIPTION-OVERVIEW.LOCKED-DIALOG.MESSAGE') - }, maxWidth: '30em' - }); - } - }, - error: (error) => { + next: (lockStatus) => { + this.isLocked = lockStatus.status; + if (this.isLocked) { + this.dialog.open(PopupNotificationDialogComponent, { + data: { + title: this.language.instant('DESCRIPTION-OVERVIEW.LOCKED-DIALOG.TITLE'), + message: this.language.instant('DESCRIPTION-OVERVIEW.LOCKED-DIALOG.MESSAGE') + }, maxWidth: '30em' + }); + } + }, + error: (error) => { this.router.navigate([this.routerUtils.generateUrl('/descriptions')]); this.httpErrorHandlingService.handleBackedRequestError(error); } - }); + }); } onFetchingDeletedCallbackError(redirectRoot: string) { @@ -336,7 +382,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni .subscribe({ complete: () => this.onDeleteCallbackSuccess(), error: (error) => this.onDeleteCallbackError(error) - }); + }); } }); } @@ -440,9 +486,9 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni }; this.planService.removeUser(planUserRemovePersist).pipe(takeUntil(this._destroyed)) .subscribe({ - next: () => this.reloadPage(), - error: (error: any) => this.httpErrorHandlingService.handleBackedRequestError(error) - }) + next: () => this.reloadPage(), + error: (error: any) => this.httpErrorHandlingService.handleBackedRequestError(error) + }) } }); } @@ -450,7 +496,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni persistStatus(status: DescriptionStatus, description: Description) { if (status.internalStatus != null && status.internalStatus === DescriptionStatusEnum.Finalized) { this.finalize(description, status.id); - } else if (status.internalStatus != null && description.status.internalStatus === DescriptionStatusEnum.Finalized){ + } else if (status.internalStatus != null && description.status.internalStatus === DescriptionStatusEnum.Finalized) { this.reverseFinalization(description, status.id); } else { // other statuses @@ -474,45 +520,45 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.descriptionService.validate([description.id]).pipe(takeUntil(this._destroyed)) .subscribe({ - next: (result) => { - if (result[0].result == DescriptionValidationOutput.Invalid) { - this.router.navigate([this.routerUtils.generateUrl(['descriptions/edit', description.id.toString(), 'finalize'], '/')]); - } else { - const dialogRef = this.dialog.open(ConfirmationDialogComponent, { - restoreFocus: false, - data: { - message: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.TITLE'), - confirmButton: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.CONFIRM'), - cancelButton: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.NEGATIVE'), - isDeleteConfirmation: false - } - }); - dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe({ - next: (result) => { - if (result) { - const descriptionStatusPersist: DescriptionStatusPersist = { - id: description.id, - statusId: statusId, - hash: description.hash - }; - this.descriptionService.persistStatus(descriptionStatusPersist).pipe(takeUntil(this._destroyed)) - .subscribe({ - next: () => { - this.reloadPage(); - this.onUpdateCallbackSuccess() - }, - error: (error: any) => this.onUpdateCallbackError(error) - - }) - } - }, - error: (error) => this.httpErrorHandlingService.handleBackedRequestError(error) - }) - } - }, - error: (error) => this.httpErrorHandlingService.handleBackedRequestError(error) - }); - + next: (result) => { + if (result[0].result == DescriptionValidationOutput.Invalid) { + this.router.navigate([this.routerUtils.generateUrl(['descriptions/edit', description.id.toString(), 'finalize'], '/')]); + } else { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + restoreFocus: false, + data: { + message: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.TITLE'), + confirmButton: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.CONFIRM'), + cancelButton: this.language.instant('DESCRIPTION-OVERVIEW.FINALIZE-DIALOG.NEGATIVE'), + isDeleteConfirmation: false + } + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe({ + next: (result) => { + if (result) { + const descriptionStatusPersist: DescriptionStatusPersist = { + id: description.id, + statusId: statusId, + hash: description.hash + }; + this.descriptionService.persistStatus(descriptionStatusPersist).pipe(takeUntil(this._destroyed)) + .subscribe({ + next: () => { + this.reloadPage(); + this.onUpdateCallbackSuccess() + }, + error: (error: any) => this.onUpdateCallbackError(error) + + }) + } + }, + error: (error) => this.httpErrorHandlingService.handleBackedRequestError(error) + }) + } + }, + error: (error) => this.httpErrorHandlingService.handleBackedRequestError(error) + }); + } hasReversableStatus(description: Description): boolean { @@ -538,28 +584,28 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni }; this.descriptionService.persistStatus(planUserRemovePersist).pipe(takeUntil(this._destroyed)) .subscribe({ - next: (data) => { - this.reloadPage(); - this.onUpdateCallbackSuccess() - }, - error: (error: any) => { - this.onUpdateCallbackError(error) - } - }); + next: (data) => { + this.reloadPage(); + this.onUpdateCallbackSuccess() + }, + error: (error: any) => { + this.onUpdateCallbackError(error) + } + }); } }); } private lookupFields(): string[] { return [ - nameof(x => x.isActive), + nameof(x => x.isActive), nameof(x => x.id), nameof(x => x.label), nameof(x => x.description), [nameof(x => x.status), nameof(x => x.id)].join('.'), [nameof(x => x.status), nameof(x => x.name)].join('.'), [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), - [nameof(x => x.status), nameof(x => x.definition), nameof(x => x.availableActions)].join('.'), + [nameof(x => x.status), nameof(x => x.definition), nameof(x => x.availableActions)].join('.'), nameof(x => x.updatedAt), nameof(x => x.belongsToCurrentTenant), nameof(x => x.hash), @@ -569,6 +615,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni [nameof(x => x.authorizationFlags), AppPermission.FinalizeDescription].join('.'), [nameof(x => x.authorizationFlags), AppPermission.InvitePlanUsers].join('.'), [nameof(x => x.authorizationFlags), AppPermission.AnnotateDescription].join('.'), + [nameof(x => x.authorizationFlags), AppPermission.EvaluateDescription].join('.'), [nameof(x => x.statusAuthorizationFlags), DescriptionStatusPermission.Edit].join('.'), diff --git a/frontend/src/app/ui/plan/overview/plan-overview.component.html b/frontend/src/app/ui/plan/overview/plan-overview.component.html index effb80c50..15d81cc6f 100644 --- a/frontend/src/app/ui/plan/overview/plan-overview.component.html +++ b/frontend/src/app/ui/plan/overview/plan-overview.component.html @@ -228,6 +228,29 @@ + + 0"> + + + + open_in_new + + + + {{ 'PLAN-OVERVIEW.ACTIONS.EVALUATE' | translate }} + + + + + + + {{ (evaluator.evaluatorId?.toUpperCase()) | translate }} + + + + + diff --git a/frontend/src/app/ui/plan/overview/plan-overview.component.scss b/frontend/src/app/ui/plan/overview/plan-overview.component.scss index 00d53e4fa..07a23a910 100644 --- a/frontend/src/app/ui/plan/overview/plan-overview.component.scss +++ b/frontend/src/app/ui/plan/overview/plan-overview.component.scss @@ -299,4 +299,10 @@ .deleted-item { color: #cf1407; +} + +.logo { + margin-right: 16px; + max-width: 24px; + max-height: 24px; } \ No newline at end of file diff --git a/frontend/src/app/ui/plan/overview/plan-overview.component.ts b/frontend/src/app/ui/plan/overview/plan-overview.component.ts index dd023ebc7..d9c031abb 100644 --- a/frontend/src/app/ui/plan/overview/plan-overview.component.ts +++ b/frontend/src/app/ui/plan/overview/plan-overview.component.ts @@ -1,36 +1,46 @@ import { Location } from '@angular/common'; import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { DescriptionStatusEnum } from '@app/core/common/enum/description-status'; -import { PlanAccessType } from '@app/core/common/enum/plan-access-type'; -import { PlanStatusEnum } from '@app/core/common/enum/plan-status'; -import { PlanUserRole } from '@app/core/common/enum/plan-user-role'; -import { PlanVersionStatus } from '@app/core/common/enum/plan-version-status'; +import { EvaluatorEntityType } from '@app/core/common/enum/evaluator-entity-type'; import { FileTransformerEntityType } from '@app/core/common/enum/file-transformer-entity-type'; import { IsActive } from '@app/core/common/enum/is-active.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { PlanAccessType } from '@app/core/common/enum/plan-access-type'; +import { PlanStatusEnum } from '@app/core/common/enum/plan-status'; +import { PlanStatusAvailableActionType } from '@app/core/common/enum/plan-status-available-action-type'; +import { PlanStatusPermission } from '@app/core/common/enum/plan-status-permission.enum'; +import { PlanUserRole } from '@app/core/common/enum/plan-user-role'; +import { PlanVersionStatus } from '@app/core/common/enum/plan-version-status'; import { DepositConfiguration } from '@app/core/model/deposit/deposit-configuration'; +import { DescriptionStatus } from '@app/core/model/description-status/description-status'; import { DescriptionTemplate } from '@app/core/model/description-template/description-template'; import { Description } from '@app/core/model/description/description'; +import { EntityDoi } from '@app/core/model/entity-doi/entity-doi'; +import { RankModel } from '@app/core/model/evaluator/evaluator-plan-model.model'; import { DescriptionTemplatesInSection, PlanBlueprint, PlanBlueprintDefinition, PlanBlueprintDefinitionSection } from '@app/core/model/plan-blueprint/plan-blueprint'; +import { PlanStatus, PlanStatusDefinition } from '@app/core/model/plan-status/plan-status'; import { BasePlan, Plan, PlanDescriptionTemplate, PlanUser, PlanUserRemovePersist, PublicPlan } from '@app/core/model/plan/plan'; import { PlanReference } from '@app/core/model/plan/plan-reference'; -import { EntityDoi } from '@app/core/model/entity-doi/entity-doi'; import { ReferenceType } from '@app/core/model/reference-type/reference-type'; import { Reference } from '@app/core/model/reference/reference'; import { User } from '@app/core/model/user/user'; import { AuthService } from '@app/core/services/auth/auth.service'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { DepositService } from '@app/core/services/deposit/deposit.service'; -import { PlanBlueprintService } from '@app/core/services/plan/plan-blueprint.service'; -import { PlanService } from '@app/core/services/plan/plan.service'; +import { EvaluatorService } from '@app/core/services/evaluator/evaluator.service'; import { FileTransformerService } from '@app/core/services/file-transformer/file-transformer.service'; import { LockService } from '@app/core/services/lock/lock.service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; import { AnalyticsService } from '@app/core/services/matomo/analytics-service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { PlanBlueprintService } from '@app/core/services/plan/plan-blueprint.service'; +import { PlanService } from '@app/core/services/plan/plan.service'; import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service'; import { ReferenceService } from '@app/core/services/reference/reference.service'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; import { UserService } from '@app/core/services/user/user.service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; import { FileUtils } from '@app/core/services/utilities/file-utils.service'; @@ -44,16 +54,11 @@ import { TranslateService } from '@ngx-translate/core'; import { map, takeUntil } from 'rxjs/operators'; import { nameof } from 'ts-simple-nameof'; import { ClonePlanDialogComponent } from '../clone-dialog/plan-clone-dialog.component'; -import { PlanDeleteDialogComponent } from '../plan-delete-dialog/plan-delete-dialog.component'; -import { PlanEditorEntityResolver } from '../plan-editor-blueprint/resolvers/plan-editor-enitity.resolver'; -import { PlanFinalizeDialogComponent, PlanFinalizeDialogOutput } from '../plan-finalize-dialog/plan-finalize-dialog.component'; import { PlanInvitationDialogComponent } from '../invitation/dialog/plan-invitation-dialog.component'; import { NewVersionPlanDialogComponent } from '../new-version-dialog/plan-new-version-dialog.component'; -import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; -import { DescriptionStatus } from '@app/core/model/description-status/description-status'; -import { PlanStatus, PlanStatusDefinition } from '@app/core/model/plan-status/plan-status'; -import { PlanStatusAvailableActionType } from '@app/core/common/enum/plan-status-available-action-type'; -import { PlanStatusPermission } from '@app/core/common/enum/plan-status-permission.enum'; +import { PlanDeleteDialogComponent } from '../plan-delete-dialog/plan-delete-dialog.component'; +import { PlanEvaluateDialogComponent } from '../plan-evaluate-dialog/plan-evaluate-dialog.component'; +import { PlanFinalizeDialogComponent, PlanFinalizeDialogOutput } from '../plan-finalize-dialog/plan-finalize-dialog.component'; @Component({ selector: 'app-plan-overview', @@ -73,6 +78,8 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { textMessage: any; selectedModel: EntityDoi; fileTransformerEntityTypeEnum = FileTransformerEntityType; + evaluatorEntityTypeEnum = EvaluatorEntityType + logos: Map = new Map(); @ViewChild('doi') doi: ElementRef; @@ -112,6 +119,9 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { private breadcrumbService: BreadcrumbService, private httpErrorHandlingService: HttpErrorHandlingService, private userService: UserService, + private evaluatorService: EvaluatorService, + private logger: LoggingService, + private sanitizer: DomSanitizer, ) { super(); } @@ -130,38 +140,38 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { this.planService.getSingle(itemId, this.lookupFields()) .pipe(takeUntil(this._destroyed)) .subscribe({ - next: (data) => { - this.breadcrumbService.addIdResolvedValue(data.id?.toString(), data.label); - - this.plan = data; - this.plan.planUsers = this.isActive ? data?.planUsers?.filter((x) => x.isActive === IsActive.Active) : data?.planUsers; - this.plan.otherPlanVersions = data.otherPlanVersions?.filter(x => x.isActive === IsActive.Active) || null; - if (this.plan.descriptions && this.isActive) { - if (this.plan.status?.internalStatus == PlanStatusEnum.Finalized) { - this.plan.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus === DescriptionStatusEnum.Finalized); - } else { - this.plan.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus !== DescriptionStatusEnum.Canceled); - } - } - if (data.entityDois && data.entityDois.length > 0) this.plan.entityDois = data.entityDois.filter(x => x.isActive === IsActive.Active); - this.selectedBlueprint = data.blueprint; - this.researchers = this.referenceService.getReferencesForTypes(this.plan?.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); - if (!this.hasDoi()) { - this.selectedModel = this.plan.entityDois[0]; - } - this.checkLockStatus(this.plan.id); - }, - error: (error: any) => { - this.httpErrorHandlingService.handleBackedRequestError(error); - - if (error.status === 404) { - return this.onFetchingDeletedCallbackError('/plans/'); - } - if (error.status === 403) { - return this.onFetchingForbiddenCallbackError('/plans/'); - } - } - }) + next: (data) => { + this.breadcrumbService.addIdResolvedValue(data.id?.toString(), data.label); + + this.plan = data; + this.plan.planUsers = this.isActive ? data?.planUsers?.filter((x) => x.isActive === IsActive.Active) : data?.planUsers; + this.plan.otherPlanVersions = data.otherPlanVersions?.filter(x => x.isActive === IsActive.Active) || null; + if (this.plan.descriptions && this.isActive) { + if (this.plan.status?.internalStatus == PlanStatusEnum.Finalized) { + this.plan.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus === DescriptionStatusEnum.Finalized); + } else { + this.plan.descriptions = data.descriptions.filter(x => x.isActive === IsActive.Active && x.status?.internalStatus !== DescriptionStatusEnum.Canceled); + } + } + if (data.entityDois && data.entityDois.length > 0) this.plan.entityDois = data.entityDois.filter(x => x.isActive === IsActive.Active); + this.selectedBlueprint = data.blueprint; + this.researchers = this.referenceService.getReferencesForTypes(this.plan?.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); + if (!this.hasDoi()) { + this.selectedModel = this.plan.entityDois[0]; + } + this.checkLockStatus(this.plan.id); + }, + error: (error: any) => { + this.httpErrorHandlingService.handleBackedRequestError(error); + + if (error.status === 404) { + return this.onFetchingDeletedCallbackError('/plans/'); + } + if (error.status === 403) { + return this.onFetchingForbiddenCallbackError('/plans/'); + } + } + }) } else if (publicId != null) { this.isNew = false; @@ -170,27 +180,27 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { this.planService.getPublicSingle(publicId, this.lookupFields()) .pipe(takeUntil(this._destroyed)) .subscribe({ - next: (data) => { - this.breadcrumbService.addExcludedParam('public', true); - this.breadcrumbService.addIdResolvedValue(data.id?.toString(), data.label); - - this.plan = data; - this.researchers = this.referenceService.getReferencesForTypes(this.plan.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); //data.planReferences is of wrong type! - if (!this.hasDoi()) { - this.selectedModel = this.plan.entityDois[0]; - } - }, - error: (error: any) => { - this.httpErrorHandlingService.handleBackedRequestError(error); - - if (error.status === 404) { - return this.onFetchingDeletedCallbackError('/explore-plans'); - } - if (error.status === 403) { - return this.onFetchingForbiddenCallbackError('/explore-plans'); - } - } - }); + next: (data) => { + this.breadcrumbService.addExcludedParam('public', true); + this.breadcrumbService.addIdResolvedValue(data.id?.toString(), data.label); + + this.plan = data; + this.researchers = this.referenceService.getReferencesForTypes(this.plan.planReferences, [this.referenceTypeService.getResearcherReferenceType()]); //data.planReferences is of wrong type! + if (!this.hasDoi()) { + this.selectedModel = this.plan.entityDois[0]; + } + }, + error: (error: any) => { + this.httpErrorHandlingService.handleBackedRequestError(error); + + if (error.status === 404) { + return this.onFetchingDeletedCallbackError('/explore-plans'); + } + if (error.status === 403) { + return this.onFetchingForbiddenCallbackError('/explore-plans'); + } + } + }); } }); if (this.isAuthenticated()) { @@ -205,9 +215,9 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { ]) .pipe(takeUntil(this._destroyed)) .subscribe({ - next: (repos) => this.depositRepos = repos, - error: () => this.depositRepos = [] - }) + next: (repos) => this.depositRepos = repos, + error: () => this.depositRepos = [] + }) this.userService.getSingle(this.authentication.userId(), [ nameof(x => x.id), @@ -218,25 +228,25 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { } } - get isActive(): boolean { - return this.plan?.isActive != IsActive.Inactive; - } + get isActive(): boolean { + return this.plan?.isActive != IsActive.Inactive; + } - get selectedPlanVersion(): number { - return this.plan?.version; - } + get selectedPlanVersion(): number { + return this.plan?.version; + } - get otherPlanVersions(): Plan[] | PublicPlan[]{ - return this.plan?.otherPlanVersions; - } + get otherPlanVersions(): Plan[] | PublicPlan[] { + return this.plan?.otherPlanVersions; + } get unauthorizedTootipText(): string { return this.language.instant('PLAN-OVERVIEW.INFOS.UNAUTHORIZED-ORCID'); } - get canEditStatus(): boolean{ - return (this.plan as Plan).statusAuthorizationFlags ?.some(x => x.toLowerCase() === PlanStatusPermission.Edit.toLowerCase()) - } + get canEditStatus(): boolean { + return (this.plan as Plan).statusAuthorizationFlags?.some(x => x.toLowerCase() === PlanStatusPermission.Edit.toLowerCase()) + } onFetchingDeletedCallbackError(redirectRoot: string) { this.router.navigate([this.routerUtils.generateUrl(redirectRoot)]); @@ -266,65 +276,119 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { } canEditPlan(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return (this.isDraftPlan()) && (authorizationFlags?.some(x => x === AppPermission.EditPlan) || this.authentication.hasPermission(AppPermission.EditPlan)) && this.isPublicView == false && this.plan.belongsToCurrentTenant != false; } canCreateNewVersion(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; - const versionStatus = !this.isPublicView ? (this.plan as Plan)?.versionStatus : null; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const versionStatus = !this.isPublicView ? (this.plan as Plan)?.versionStatus : null; return (authorizationFlags?.some(x => x === AppPermission.CreateNewVersionPlan) || this.authentication.hasPermission(AppPermission.CreateNewVersionPlan)) && versionStatus === PlanVersionStatus.Current && this.isPublicView == false && this.plan.belongsToCurrentTenant != false; } canDeletePlan(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return ( - this.isActive && - (authorizationFlags?.some(x => x === AppPermission.DeletePlan) || this.authentication.hasPermission(AppPermission.DeletePlan)) && - this.isPublicView == false && this.plan.belongsToCurrentTenant != false - ) + this.isActive && + (authorizationFlags?.some(x => x === AppPermission.DeletePlan) || this.authentication.hasPermission(AppPermission.DeletePlan)) && + this.isPublicView == false && this.plan.belongsToCurrentTenant != false + ) } canClonePlan(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return ( - authorizationFlags?.some(x => x === AppPermission.ClonePlan) || - this.authentication.hasPermission(AppPermission.ClonePlan) || - (this.authentication.hasPermission(AppPermission.PublicClonePlan) && this.isPublicView) - ); + authorizationFlags?.some(x => x === AppPermission.ClonePlan) || + this.authentication.hasPermission(AppPermission.ClonePlan) || + (this.authentication.hasPermission(AppPermission.PublicClonePlan) && this.isPublicView) + ); } canFinalizePlan(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return ( - this.isActive && - (authorizationFlags?.some(x => x === AppPermission.FinalizePlan) || this.authentication.hasPermission(AppPermission.FinalizePlan))) && - this.isPublicView == false && this.plan.belongsToCurrentTenant != false; + this.isActive && + (authorizationFlags?.some(x => x === AppPermission.FinalizePlan) || this.authentication.hasPermission(AppPermission.FinalizePlan))) && + this.isPublicView == false && this.plan.belongsToCurrentTenant != false; } canExportPlan(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return (authorizationFlags?.some(x => x === AppPermission.ExportPlan) || this.authentication.hasPermission(AppPermission.ExportPlan)) && - this.plan?.status?.definition?.availableActions?.filter(x => x === PlanStatusAvailableActionType.Export).length > 0; - + this.plan?.status?.definition?.availableActions?.filter(x => x === PlanStatusAvailableActionType.Export).length > 0; + } + canEvaluatePlan(): boolean { + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + return (authorizationFlags?.some(x => x === AppPermission.EvaluatePlan) || this.authentication.hasPermission(AppPermission.EvaluatePlan)); + } + + // onEvaluatePlan(planId: Guid, evaluatorId: string, format: string, isPublicView: boolean) { + // this.evaluatorService.rankPlan(planId, evaluatorId, format).subscribe( + // (response: RankModel) => { + + // const dialogRef = this.dialog.open(PlanEvaluateDialogComponent, { + // data: { rankData: response } + // }); + + // dialogRef.afterClosed().subscribe(result => { + // this.logger.debug("Dialog closed with result:", result); + // }); + // }, + // error => { + + // this.logger.error("Error ranking plan:", error); + // } + // ); + // } + + onEvaluatePlan(planId: Guid, evaluatorId: string, format: string, isPublicView: boolean) { + this.evaluatorService.rankPlan(planId, evaluatorId, format).subscribe( + (response: RankModel) => { + this.evaluatorService.getLogo(evaluatorId).subscribe( + (logo: string) => { + + this.logos.set(evaluatorId, this.sanitizer.bypassSecurityTrustResourceUrl('data:image/png;base64, ' + logo)); + + const dialogRef = this.dialog.open(PlanEvaluateDialogComponent, { + data: { + rankData: response, + } + }); + + dialogRef.afterClosed().subscribe(result => { + this.logger.debug("Dialog closed with result:", result); + }); + }, + error => { + this.logger.error("Error fetching evaluator logo:", error); + } + ); + }, + error => { + this.logger.error("Error ranking plan:", error); + } + ); + } + + canInvitePlanUsers(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return ( - this.isActive && - (authorizationFlags?.some(x => x === AppPermission.InvitePlanUsers) || this.authentication.hasPermission(AppPermission.InvitePlanUsers))) && - this.isPublicView == false && this.plan.belongsToCurrentTenant != false; + this.isActive && + (authorizationFlags?.some(x => x === AppPermission.InvitePlanUsers) || this.authentication.hasPermission(AppPermission.InvitePlanUsers))) && + this.isPublicView == false && this.plan.belongsToCurrentTenant != false; } canAssignPlanUsers(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return (authorizationFlags?.some(x => x === AppPermission.AssignPlanUsers) || this.authentication.hasPermission(AppPermission.AssignPlanUsers)) && this.isPublicView == false && this.plan.belongsToCurrentTenant != false && - (this.plan.status.internalStatus == null || this.plan.status.internalStatus != PlanStatusEnum.Finalized); + (this.plan.status.internalStatus == null || this.plan.status.internalStatus != PlanStatusEnum.Finalized); } canDepositPlan(): boolean { - const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; + const authorizationFlags = !this.isPublicView ? (this.plan as Plan).authorizationFlags : []; return (authorizationFlags?.some(x => x === AppPermission.DepositPlan) || this.authentication.hasPermission(AppPermission.DepositPlan)) && this.isPublicView == false && this.plan.belongsToCurrentTenant != false && this.plan?.status?.definition?.availableActions?.filter(x => x === PlanStatusAvailableActionType.Deposit).length > 0; } @@ -395,7 +459,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { .subscribe({ complete: () => this.onDeleteCallbackSuccess(), error: (error) => this.onDeleteCallbackError(error) - }); + }); } }); } @@ -453,17 +517,17 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { persistStatus(status: PlanStatus) { if (status.internalStatus != null && status.internalStatus === PlanStatusEnum.Finalized) { this.finalize(status.id); - } else if (this.plan.status.internalStatus === PlanStatusEnum.Finalized){ + } else if (this.plan.status.internalStatus === PlanStatusEnum.Finalized) { this.reverseFinalization(status.id); } else { // other statuses this.planService.setStatus(this.plan.id, status.id).pipe(takeUntil(this._destroyed)) - .subscribe({ - complete: () => {this.reloadPage(); this.onUpdateCallbackSuccess()}, - error:(error: any) => { - this.onUpdateCallbackError(error) - } - }); + .subscribe({ + complete: () => { this.reloadPage(); this.onUpdateCallbackSuccess() }, + error: (error: any) => { + this.onUpdateCallbackError(error) + } + }); } } @@ -482,14 +546,14 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { this.planService.setStatus(this.plan.id, newStatusId, result.descriptionsToBeFinalized) .pipe(takeUntil(this._destroyed)) .subscribe({ - complete: () => { - this.reloadPage(); - this.onUpdateCallbackSuccess() - }, - error: (error: any) => { - this.onUpdateCallbackError(error) - } - }); + complete: () => { + this.reloadPage(); + this.onUpdateCallbackSuccess() + }, + error: (error: any) => { + this.onUpdateCallbackError(error) + } + }); } }); @@ -551,11 +615,11 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { if (result) { this.planService.setStatus(this.plan.id, newStatusId).pipe(takeUntil(this._destroyed)) .subscribe({ - complete: () => {this.reloadPage(); this.onUpdateCallbackSuccess()}, - error:(error: any) => { - this.onUpdateCallbackError(error) - } - }); + complete: () => { this.reloadPage(); this.onUpdateCallbackSuccess() }, + error: (error: any) => { + this.onUpdateCallbackError(error) + } + }); } }); } @@ -589,14 +653,14 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { }; this.planService.removeUser(planUserRemovePersist).pipe(takeUntil(this._destroyed)) .subscribe({ - complete: () => { - this.reloadPage(); - this.onUpdateCallbackSuccess() - }, - error: (error: any) => { - this.onUpdateCallbackError(error) - } - }); + complete: () => { + this.reloadPage(); + this.onUpdateCallbackSuccess() + }, + error: (error: any) => { + this.onUpdateCallbackError(error) + } + }); } }); } @@ -638,34 +702,34 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { checkLockStatus(id: Guid) { this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed)) .subscribe({ - next: (lockStatus) => { - this.isLocked = lockStatus.status; - if (this.isLocked) { - this.dialog.open(PopupNotificationDialogComponent, { - data: { - title: this.language.instant('PLAN-OVERVIEW.LOCKED-DIALOG.TITLE'), - message: this.language.instant('PLAN-OVERVIEW.LOCKED-DIALOG.MESSAGE') - }, maxWidth: '30em' - }); - } - }, - error: (error) => { + next: (lockStatus) => { + this.isLocked = lockStatus.status; + if (this.isLocked) { + this.dialog.open(PopupNotificationDialogComponent, { + data: { + title: this.language.instant('PLAN-OVERVIEW.LOCKED-DIALOG.TITLE'), + message: this.language.instant('PLAN-OVERVIEW.LOCKED-DIALOG.MESSAGE') + }, maxWidth: '30em' + }); + } + }, + error: (error) => { this.router.navigate([this.routerUtils.generateUrl('/plans')]); this.httpErrorHandlingService.handleBackedRequestError(error); } - }); + }); } private lookupFields(): string[] { return [ - nameof(x => x.isActive), + nameof(x => x.isActive), nameof(x => x.id), nameof(x => x.label), nameof(x => x.description), [nameof(x => x.status), nameof(x => x.id)].join('.'), [nameof(x => x.status), nameof(x => x.name)].join('.'), - [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), - [nameof(x => x.status), nameof(x => x.definition), nameof(x => x.availableActions)].join('.'), + [nameof(x => x.status), nameof(x => x.internalStatus)].join('.'), + [nameof(x => x.status), nameof(x => x.definition), nameof(x => x.availableActions)].join('.'), nameof(x => x.accessType), nameof(x => x.version), nameof(x => x.versionStatus), @@ -679,6 +743,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { [nameof(x => x.authorizationFlags), AppPermission.ClonePlan].join('.'), [nameof(x => x.authorizationFlags), AppPermission.FinalizePlan].join('.'), [nameof(x => x.authorizationFlags), AppPermission.ExportPlan].join('.'), + [nameof(x => x.authorizationFlags), AppPermission.EvaluatePlan].join('.'), [nameof(x => x.authorizationFlags), AppPermission.InvitePlanUsers].join('.'), [nameof(x => x.authorizationFlags), AppPermission.AssignPlanUsers].join('.'), [nameof(x => x.authorizationFlags), AppPermission.EditPlan].join('.'), diff --git a/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.html b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.html new file mode 100644 index 000000000..b3362bdf2 --- /dev/null +++ b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.html @@ -0,0 +1,35 @@ +{{ 'PLAN-EVALUATE-DIALOG.HEADER' | translate }} + + + + {{'PLAN-EVALUATE-DIALOG.DETAILS-SUB-HEADER' | translate}} + + + + + {{'PLAN-EVALUATE-DIALOG.RANK-BODY' | translate}} + + + + {{'PLAN-EVALUATE-DIALOG.DETAILS-BODY' | translate}} + + + + + + {{'PLAN-EVALUATE-DIALOG.MESSAGES-BODY' | translate}} + + + + + + {{ entry.key }}: {{ entry.value }} + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.scss b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.scss new file mode 100644 index 000000000..b44d417e6 --- /dev/null +++ b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.scss @@ -0,0 +1,8 @@ +.dialog-content { + display: flex; + flex-direction: column; /* Stack form fields vertically */ + } + + mat-form-field { + margin-bottom: 16px; /* Space between fields */ + } \ No newline at end of file diff --git a/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.spec.ts b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.spec.ts new file mode 100644 index 000000000..464a8f275 --- /dev/null +++ b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PlanEvaluateDialogComponent } from './plan-evaluate-dialog.component'; + +describe('PlanEvaluateDialogComponent', () => { + let component: PlanEvaluateDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PlanEvaluateDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PlanEvaluateDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.ts b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.ts new file mode 100644 index 000000000..e966326ee --- /dev/null +++ b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { RankModel } from '@app/core/model/evaluator/evaluator-plan-model.model'; + +@Component({ + selector: 'app-plan-evaluate-dialog', + templateUrl: './plan-evaluate-dialog.component.html', + styleUrl: './plan-evaluate-dialog.component.scss' +}) +export class PlanEvaluateDialogComponent { + // Injecting the dialog data into the component + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { rankData: RankModel }, + ) { } +} diff --git a/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.module.ts b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.module.ts new file mode 100644 index 000000000..559be59f7 --- /dev/null +++ b/frontend/src/app/ui/plan/plan-evaluate-dialog/plan-evaluate-dialog.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { NgIf } from '@angular/common'; +import { PlanEvaluateDialogComponent } from './plan-evaluate-dialog.component'; +import { CommonUiModule } from '@common/ui/common-ui.module'; + +@NgModule({ + imports: [ + CommonModule, + MatCardModule, + MatButtonModule, + MatInputModule, + MatFormFieldModule, + NgIf, + CommonUiModule, + ], + declarations: [PlanEvaluateDialogComponent], + exports: [PlanEvaluateDialogComponent] + }) + export class EvaluatePlanDialogModule { } \ No newline at end of file diff --git a/frontend/src/app/ui/plan/plan.module.ts b/frontend/src/app/ui/plan/plan.module.ts index cbb37bdcb..f1a45e310 100644 --- a/frontend/src/app/ui/plan/plan.module.ts +++ b/frontend/src/app/ui/plan/plan.module.ts @@ -4,6 +4,7 @@ import { PlanRoutingModule, PublicPlanRoutingModule } from '@app/ui/plan/plan.ro import { CommonFormsModule } from '@common/forms/common-forms.module'; import { CommonUiModule } from '@common/ui/common-ui.module'; import { InvitationAcceptedComponent } from './invitation/accepted/plan-invitation-accepted.component'; +import { EvaluatePlanDialogModule } from './plan-evaluate-dialog/plan-evaluate-dialog.module'; @NgModule({ imports: [ @@ -11,6 +12,7 @@ import { InvitationAcceptedComponent } from './invitation/accepted/plan-invitati CommonFormsModule, FormattingModule, PlanRoutingModule, + EvaluatePlanDialogModule, ], declarations: [ InvitationAcceptedComponent diff --git a/frontend/src/assets/config/config.json b/frontend/src/assets/config/config.json index a62b09393..9e55939f1 100644 --- a/frontend/src/assets/config/config.json +++ b/frontend/src/assets/config/config.json @@ -70,7 +70,7 @@ "name": "Default", "providerClass": "defaultIcon" }, - "authProviders": [ + "authProviders": [ { "name": "google", "providerClass": "googleIcon", @@ -99,14 +99,14 @@ "externalUrl": "/splash/resources/co-branding.html", "accessLevel": "public" }, - { - "title": "SIDE-BAR.SUPPORT", + { + "title": "SIDE-BAR.SUPPORT", "icon": "help", "routerPath": "/contact-support", "accessLevel": "authenticated" }, - { - "title": "SIDE-BAR.SUPPORT", + { + "title": "SIDE-BAR.SUPPORT", "icon": "help", "externalUrl": "/splash/contact.html", "accessLevel": "unauthenticated" diff --git a/frontend/src/assets/i18n/baq.json b/frontend/src/assets/i18n/baq.json index 8eeb7dfa0..4055205ce 100644 --- a/frontend/src/assets/i18n/baq.json +++ b/frontend/src/assets/i18n/baq.json @@ -190,6 +190,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -856,7 +859,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1002,7 +1006,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1941,7 +1946,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "DOIa ez da behar bezala sortu", "SUCCESSFUL-DOI": "DOIa ondo sortu da", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1959,6 +1966,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG":{ + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG":{ + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 6bc0b1a94..1942bd8bd 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "DOI Erstellung fehlgeschlagen", "SUCCESSFUL-DOI": "DOI Erstellung erfolgreich", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG":{ + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG":{ + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 886c1d45f..843e02edb 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -857,7 +860,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1003,7 +1007,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Unsuccessful DOI creation", "SUCCESSFUL-DOI": "Successful DOI creation", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION": "Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG":{ + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG":{ + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/es.json b/frontend/src/assets/i18n/es.json index a3ac07187..f4155d37f 100644 --- a/frontend/src/assets/i18n/es.json +++ b/frontend/src/assets/i18n/es.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Fallo en la creación del DOI", "SUCCESSFUL-DOI": "Creación del DOI correcta", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/gr.json b/frontend/src/assets/i18n/gr.json index f61e136b6..1fd01a474 100644 --- a/frontend/src/assets/i18n/gr.json +++ b/frontend/src/assets/i18n/gr.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Αποτυχία δημιουργίας Μονοσήμαντων Αναγνωριστικών Ψηφιακών Αντικειμένων (DOI)", "SUCCESSFUL-DOI": "Επιτυχία δημιουργίας Μονοσήμαντων Αναγνωριστικών Ψηφιακών Αντικειμένων (DOI)", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/hr.json b/frontend/src/assets/i18n/hr.json index 7305db63f..fc4645654 100644 --- a/frontend/src/assets/i18n/hr.json +++ b/frontend/src/assets/i18n/hr.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Neuspješno generiran DOI", "SUCCESSFUL-DOI": "Uspješno generiran DOI", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/pl.json b/frontend/src/assets/i18n/pl.json index 5edb26acc..611b8d5b5 100644 --- a/frontend/src/assets/i18n/pl.json +++ b/frontend/src/assets/i18n/pl.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Nie udało się utworzyć DOI", "SUCCESSFUL-DOI": "Utworzono DOI", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/pt.json b/frontend/src/assets/i18n/pt.json index 25cd8afaa..a80d87dbf 100644 --- a/frontend/src/assets/i18n/pt.json +++ b/frontend/src/assets/i18n/pt.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Criação de DOI sem sucesso", "SUCCESSFUL-DOI": "Criação de DOI com sucesso", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/sk.json b/frontend/src/assets/i18n/sk.json index 48dab0b5d..579ddcb40 100644 --- a/frontend/src/assets/i18n/sk.json +++ b/frontend/src/assets/i18n/sk.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Neúspešné vytvorenie DOI", "SUCCESSFUL-DOI": "Úspešné vytvorenie DOI", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/sr.json b/frontend/src/assets/i18n/sr.json index 9d5b14196..392d45ec0 100644 --- a/frontend/src/assets/i18n/sr.json +++ b/frontend/src/assets/i18n/sr.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Neuspešno registrovan DOI", "SUCCESSFUL-DOI": "Uspešno registrovan DOI", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": { diff --git a/frontend/src/assets/i18n/tr.json b/frontend/src/assets/i18n/tr.json index 07a54e030..592b60b0e 100644 --- a/frontend/src/assets/i18n/tr.json +++ b/frontend/src/assets/i18n/tr.json @@ -193,6 +193,9 @@ "JSON": "RDA JSON", "DOCX": "Document" }, + "EVALUATOR": { + "FAIR": "FAIR Evaluator" + }, "LANGUAGES": { "en": "English", "gr": "Greek", @@ -859,7 +862,8 @@ "NEW-VERSION": "Start new version", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "UNDO-FINALIZATION-DIALOG": { "TITLE": "Undo Finalization?", @@ -1005,7 +1009,8 @@ "EXPORT": "Export", "INVITE-SHORT": "Invite", "REMOVE-AUTHOR": "Remove", - "PREVIEW": "Preview" + "PREVIEW": "Preview", + "EVALUATE": "Evaluate" }, "COPY-DIALOG": { "COPY": "Copy", @@ -1944,7 +1949,9 @@ "SNACK-BAR": { "UNSUCCESSFUL-DOI": "Başarısız DOI oluşturma", "SUCCESSFUL-DOI": "Başarılı DOI oluşumu", - "SUCCESSFUL-PLAN-CONTACT": "User added" + "SUCCESSFUL-PLAN-CONTACT": "User added", + "SUCCESSFUL-EVALUATION": "Successful evaluation", + "UNSUCCESSFUL-EVALUATION":"Unsuccessful evaluation" }, "DESCRIPTION-TEMPLATE-LIST": { "TITLE": "Available Description Templates", @@ -1962,6 +1969,20 @@ "MESSAGE": "Somebody else is modifying the Plan at this moment. You may view the Plan but you cannot make any changes." } }, + "PLAN-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, + "DESCRIPTION-EVALUATE-DIALOG": { + "HEADER": "Ranking Information", + "DETAILS-SUB-HEADER": "Details", + "RANK-BODY": "Rank", + "DETAILS-BODY": "Details", + "MESSAGES-BODY": "Messages" + }, "REFERENCE-FIELD": { "COULD-NOT-FIND-MESSAGE": "Couldn't find it?", "ACTIONS": {
{{ 'DESCRIPTION-OVERVIEW.ACTIONS.EVALUATE' | translate }}
{{ 'PLAN-OVERVIEW.ACTIONS.EVALUATE' | translate }}