diff --git a/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingRippler.java b/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingRippler.java new file mode 100644 index 0000000000..e51e201740 --- /dev/null +++ b/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingRippler.java @@ -0,0 +1,466 @@ +package org.finos.waltz.data.assessment_rating; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.waltz.common.Checks; +import org.finos.waltz.common.MapUtilities; +import org.finos.waltz.common.SetUtilities; +import org.finos.waltz.data.settings.SettingsDao; +import org.finos.waltz.model.EntityKind; +import org.finos.waltz.model.EntityLifecycleStatus; +import org.finos.waltz.model.EntityReference; +import org.finos.waltz.model.PairDiffResult; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobConfiguration; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobStep; +import org.finos.waltz.model.assessment_definition.ImmutableAssessmentRipplerJobConfiguration; +import org.finos.waltz.schema.Tables; +import org.finos.waltz.schema.tables.Actor; +import org.finos.waltz.schema.tables.Application; +import org.finos.waltz.schema.tables.AssessmentDefinition; +import org.finos.waltz.schema.tables.AssessmentRating; +import org.finos.waltz.schema.tables.ChangeInitiative; +import org.finos.waltz.schema.tables.EndUserApplication; +import org.finos.waltz.schema.tables.EntityRelationship; +import org.finos.waltz.schema.tables.LogicalFlow; +import org.finos.waltz.schema.tables.Measurable; +import org.finos.waltz.schema.tables.MeasurableRating; +import org.finos.waltz.schema.tables.PhysicalFlow; +import org.finos.waltz.schema.tables.PhysicalSpecDataType; +import org.finos.waltz.schema.tables.PhysicalSpecification; +import org.finos.waltz.schema.tables.RatingScheme; +import org.finos.waltz.schema.tables.RatingSchemeItem; +import org.finos.waltz.schema.tables.records.AssessmentDefinitionRecord; +import org.finos.waltz.schema.tables.records.AssessmentRatingRecord; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Record4; +import org.jooq.Result; +import org.jooq.Select; +import org.jooq.impl.DSL; +import org.jooq.lambda.tuple.Tuple2; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.finos.waltz.common.Checks.checkTrue; +import static org.finos.waltz.common.DateTimeUtilities.nowUtcTimestamp; +import static org.finos.waltz.common.JacksonUtilities.getJsonMapper; +import static org.finos.waltz.common.ListUtilities.asList; +import static org.finos.waltz.common.StringUtilities.safeEq; +import static org.finos.waltz.data.JooqUtilities.summarizeResults; +import static org.finos.waltz.model.EntityReference.mkRef; +import static org.finos.waltz.model.PairDiffResult.mkPairDiff; +import static org.jooq.lambda.tuple.Tuple.tuple; + +@Service +public class AssessmentRatingRippler { + + private static final Logger LOG = LoggerFactory.getLogger(AssessmentRatingRippler.class); + + private static final PhysicalFlow pf = Tables.PHYSICAL_FLOW; + private static final PhysicalSpecification ps = Tables.PHYSICAL_SPECIFICATION; + private static final PhysicalSpecDataType psdt = Tables.PHYSICAL_SPEC_DATA_TYPE; + private static final AssessmentDefinition ad = Tables.ASSESSMENT_DEFINITION; + private static final AssessmentRating ar = Tables.ASSESSMENT_RATING; + private static final Application app = Tables.APPLICATION; + private static final Actor act = Tables.ACTOR; + private static final LogicalFlow lf = Tables.LOGICAL_FLOW; + private static final RatingSchemeItem rsi = Tables.RATING_SCHEME_ITEM; + private static final RatingScheme rs = Tables.RATING_SCHEME; + private static final MeasurableRating mr = Tables.MEASURABLE_RATING; + private static final Measurable m = Tables.MEASURABLE; + private static final EntityRelationship er = Tables.ENTITY_RELATIONSHIP; + private static final ChangeInitiative ci = Tables.CHANGE_INITIATIVE; + private static final EndUserApplication euda = Tables.END_USER_APPLICATION; + private static final Set flowNodeEntities = SetUtilities.asSet(EntityKind.APPLICATION, EntityKind.ACTOR, EntityKind.END_USER_APPLICATION); + + private final DSLContext dsl; + private final SettingsDao settingsDao; + + @Autowired + public AssessmentRatingRippler(DSLContext dsl, + SettingsDao settingsDao) { + this.dsl = dsl; + this.settingsDao = settingsDao; + } + + + public static AssessmentRipplerJobConfiguration parseConfig(String name, + String value) throws JsonProcessingException { + ObjectMapper jsonMapper = getJsonMapper(); + AssessmentRipplerJobStep[] steps = jsonMapper.readValue(value, AssessmentRipplerJobStep[].class); + + return ImmutableAssessmentRipplerJobConfiguration + .builder() + .name(name) + .steps(asList(steps)) + .build(); + } + + + /** + * Ripple all assessments configured in the settings table + * + * @return the number of steps taken, where a step is a source assessment def and a target assessment def + */ + public final Long rippleAssessments() { + Set rippleConfig = findRippleConfig(); + + return dsl.transactionResult(ctx -> { + DSLContext tx = ctx.dsl(); + return rippleConfig + .stream() + .flatMap(config -> config.steps().stream()) + .map(step -> { + rippleAssessment( + tx, + "waltz", + "waltz-assessment-rippler", + step.fromDef(), + step.toDef()); + return 1; + }) + .count(); + }); + } + + + public static void rippleAssessment(DSLContext tx, + String userId, + String provenance, + String from, + String to) { + + Map defs = tx + .selectFrom(ad) + .where(ad.EXTERNAL_ID.in(from, to)) + .fetchMap(r -> r.get(ad.EXTERNAL_ID)); + + AssessmentDefinitionRecord fromDef = defs.get(from); + Checks.checkNotNull(fromDef, "Cannot ripple assessment as definition: %s not found", from); + AssessmentDefinitionRecord toDef = defs.get(to); + Checks.checkNotNull(toDef, "Cannot ripple assessment as definition: %s not found", toDef); + rippleAssessment(tx, userId, provenance, fromDef, toDef); + } + + + private static void rippleAssessment(DSLContext tx, + String userId, + String provenance, + AssessmentDefinitionRecord from, + AssessmentDefinitionRecord to) { + checkTrue( + from.getRatingSchemeId().equals(to.getRatingSchemeId()), + "Assessments must share a rating scheme when rippling (%s -> %s)", + from.getName(), + to.getName()); + + Tuple2 kinds = tuple( + EntityKind.valueOf(from.getEntityKind()), + EntityKind.valueOf(to.getEntityKind())); + + if (kinds.equals(tuple(EntityKind.PHYSICAL_SPECIFICATION, EntityKind.PHYSICAL_FLOW))) { + // PHYSICAL_SPEC -> PHYSICAL_FLOW + rippleAssessments( + tx, + userId, + provenance, + from, + to, + tx.select(pf.ID, ar.RATING_ID, ps.ID, ps.NAME) + .from(ar) + .innerJoin(ps) + .on(ps.ID.eq(ar.ENTITY_ID)) + .innerJoin(pf) + .on(pf.SPECIFICATION_ID.eq(ps.ID)) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId()) + .and(ps.IS_REMOVED.isFalse()))); + } else if (kinds.equals(tuple(EntityKind.PHYSICAL_FLOW, EntityKind.LOGICAL_DATA_FLOW))) { + // PHYSICAL_FLOW -> LOGICAL + rippleAssessments( + tx, + userId, + provenance, + from, + to, + tx.select(lf.ID, ar.RATING_ID, pf.ID, pf.NAME) + .from(ar) + .innerJoin(pf).on(pf.ID.eq(ar.ENTITY_ID)) + .innerJoin(lf).on(lf.ID.eq(pf.LOGICAL_FLOW_ID)) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId()) + .and(pf.IS_REMOVED.isFalse()) + .and(pf.ENTITY_LIFECYCLE_STATUS.ne(EntityLifecycleStatus.REMOVED.name())))); + } else if (kinds.v1 == EntityKind.LOGICAL_DATA_FLOW && (flowNodeEntities.contains(kinds.v2))) { + // LOGICAL -> APP | ACTOR | END_USER_APP + Condition lfIsActiveCondition = lf.IS_REMOVED.isFalse() + .and(lf.ENTITY_LIFECYCLE_STATUS.ne(EntityLifecycleStatus.REMOVED.name())); + Actor sourceActor = act.as("source_actor"); + Actor targetActor = act.as("target_actor"); + EndUserApplication sourceEuda = euda.as("source_euda"); + EndUserApplication targetEuda = euda.as("target_euda"); + Application sourceApp = app.as("source_app"); + Application targetApp = app.as("target_app"); + Condition sourceActorJoinCondition = lf.SOURCE_ENTITY_KIND.eq(EntityKind.ACTOR.name()).and(sourceActor.ID.eq(lf.SOURCE_ENTITY_ID)); + Condition targetActorJoinCondition = lf.TARGET_ENTITY_KIND.eq(EntityKind.ACTOR.name()).and(targetActor.ID.eq(lf.TARGET_ENTITY_ID)); + Condition sourceAppJoinCondition = lf.SOURCE_ENTITY_KIND.eq(EntityKind.APPLICATION.name()).and(sourceApp.ID.eq(lf.SOURCE_ENTITY_ID)); + Condition targetAppJoinCondition = lf.TARGET_ENTITY_KIND.eq(EntityKind.APPLICATION.name()).and(targetApp.ID.eq(lf.TARGET_ENTITY_ID)); + Condition sourceEudaJoinCondition = lf.SOURCE_ENTITY_KIND.eq(EntityKind.END_USER_APPLICATION.name()).and(sourceApp.ID.eq(lf.SOURCE_ENTITY_ID)); + Condition targetEudaJoinCondition = lf.TARGET_ENTITY_KIND.eq(EntityKind.END_USER_APPLICATION.name()).and(targetApp.ID.eq(lf.TARGET_ENTITY_ID)); + Field sourceName = DSL.coalesce(sourceApp.NAME, sourceActor.NAME, sourceEuda.NAME, DSL.value("??")); + Field targetName = DSL.coalesce(targetApp.NAME, targetActor.NAME, targetEuda.NAME, DSL.value("??")); + Field flowDesc = DSL.concat( + DSL.value("Flow: "), + sourceName, + DSL.value(" -> "), + targetName); + rippleAssessments( + tx, + userId, + provenance, + from, + to, + tx.select(lf.SOURCE_ENTITY_ID, ar.RATING_ID, lf.ID, flowDesc) + .from(ar) + .innerJoin(lf).on(lf.ID.eq(ar.ENTITY_ID)) + .leftJoin(sourceApp).on(sourceAppJoinCondition) + .leftJoin(targetApp).on(targetAppJoinCondition) + .leftJoin(sourceActor).on(sourceActorJoinCondition) + .leftJoin(targetActor).on(targetActorJoinCondition) + .leftJoin(sourceEuda).on(sourceEudaJoinCondition) + .leftJoin(targetEuda).on(targetEudaJoinCondition) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId()) + .and(lf.SOURCE_ENTITY_KIND.eq(kinds.v2.name())) + .and(lfIsActiveCondition)) + .union(DSL + .select(lf.TARGET_ENTITY_ID, ar.RATING_ID, lf.ID, flowDesc) + .from(ar) + .innerJoin(lf).on(lf.ID.eq(ar.ENTITY_ID)) + .leftJoin(sourceApp).on(sourceAppJoinCondition) + .leftJoin(targetApp).on(targetAppJoinCondition) + .leftJoin(sourceActor).on(sourceActorJoinCondition) + .leftJoin(targetActor).on(targetActorJoinCondition) + .leftJoin(sourceEuda).on(sourceEudaJoinCondition) + .leftJoin(targetEuda).on(targetEudaJoinCondition) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId()) + .and(lf.TARGET_ENTITY_KIND.eq(kinds.v2.name())) + .and(lfIsActiveCondition)))); + } else if (kinds.v1 == EntityKind.MEASURABLE && kinds.v2 == EntityKind.APPLICATION) { + // MEASURABLE -> APPLICATION + rippleAssessments( + tx, + userId, + provenance, + from, + to, + tx.select(mr.ENTITY_ID, ar.RATING_ID, m.ID, m.NAME) + .from(ar) + .innerJoin(mr).on(mr.MEASURABLE_ID.eq(ar.ENTITY_ID)) + .innerJoin(m).on(m.ID.eq(mr.MEASURABLE_ID)) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId())) + .and(mr.ENTITY_KIND.eq(EntityKind.APPLICATION.name()))); + } else if (kinds.v1 == EntityKind.MEASURABLE && kinds.v2 == EntityKind.MEASURABLE_RATING) { + // MEASURABLE -> MEASURABLE_RATING + rippleAssessments( + tx, + userId, + provenance, + from, + to, + tx.select(mr.ID, ar.RATING_ID, m.ID, m.NAME) + .from(ar) + .innerJoin(mr).on(mr.MEASURABLE_ID.eq(ar.ENTITY_ID)) + .innerJoin(m).on(m.ID.eq(mr.MEASURABLE_ID)) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId())) + .and(mr.ENTITY_KIND.eq(EntityKind.APPLICATION.name()))); + } else if (kinds.v1 == EntityKind.CHANGE_INITIATIVE && kinds.v2 == EntityKind.APPLICATION) { + // CHANGE_INITIATIVE -> APPLICATION + rippleAssessments( + tx, + userId, + provenance, + from, + to, + tx.select(er.ID_B, ar.RATING_ID, ci.ID, ci.NAME) + .from(er) + .innerJoin(ci).on(ci.ID.eq(er.ID_A).and(er.KIND_A.eq(EntityKind.CHANGE_INITIATIVE.name()))) + .innerJoin(ar).on(ar.ENTITY_ID.eq(ci.ID) + .and(ar.ENTITY_KIND.eq(EntityKind.CHANGE_INITIATIVE.name())) + .and(ar.ASSESSMENT_DEFINITION_ID.eq(from.getId())))); + } else { + throw new UnsupportedOperationException(format( + "Cannot ripple assessment from kind: %s to kind: %s", + kinds.v1, + kinds.v2)); + } + } + + + private static void rippleAssessments(DSLContext tx, + String userId, + String provenance, + AssessmentDefinitionRecord from, + AssessmentDefinitionRecord to, + Select> targetAndRatingProvider) { + Timestamp now = nowUtcTimestamp(); + Set required = MapUtilities + .groupAndThen( + targetAndRatingProvider.fetch(), + r -> tuple(r.get(0, Long.class), r.get(1, Long.class)), + xs -> xs.stream() + .map(x -> mkRef( + EntityKind.valueOf(from.getEntityKind()), + x.get(2, Long.class), + x.get(3, String.class))) + .sorted(Comparator.comparing(d -> d.name().orElse("??"))) + .collect(Collectors.toList())) + .entrySet() + .stream() + .map(kv -> { + String desc = calcDescription(from, kv.getValue()); + + AssessmentRatingRecord record = tx.newRecord(ar); + record.setAssessmentDefinitionId(to.getId()); + record.setEntityId(kv.getKey().v1); + record.setEntityKind(to.getEntityKind()); + record.setRatingId(kv.getKey().v2); + record.setLastUpdatedAt(now); + record.setIsReadonly(true); + record.setLastUpdatedBy(userId); + record.setProvenance(provenance); + record.setDescription(desc); + return record; + }) + .collect(toSet()); + + Result existing = tx + .selectFrom(ar) + .where(ar.ASSESSMENT_DEFINITION_ID.eq(to.getId())) + .fetch(); + + PairDiffResult diff = mkPairDiff( + existing, + required, + a -> tuple(a.getEntityId(), a.getRatingId()), + b -> tuple(b.getEntityId(), b.getRatingId()), + (a, b) -> safeEq(a.getDescription(), b.getDescription())); + + int insertCount = summarizeResults(tx.batchInsert(diff.otherOnly()).execute()); + int rmCount = summarizeResults(tx.batchDelete(diff.waltzOnly()).execute()); + int updCount = summarizeResults(diff.differingIntersection() + .stream() + .map(t -> tx.update(ar).set(ar.DESCRIPTION, t.v2.getDescription()).where(ar.ID.eq(t.v1.getId()))) + .collect(Collectors.collectingAndThen(toSet(), tx::batch)) + .execute()); + + LOG.info(format( + "Assessment Rippler: %s -> %s, created: %d ratings, removed: %d ratings, updated: %d ratings", + from.getExternalId(), + to.getExternalId(), + insertCount, + rmCount, + updCount)); + } + + + private static String calcDescription(AssessmentDefinitionRecord from, + List sourceRefs) { + String fullDesc = format( + "Rating derived from %s assessment/s on:\n\n%s", + from.getName(), + sourceRefs + .stream() + .map(ref -> format( + "- %s", + toLink(ref))) + .collect(joining("\n"))); + + String cutoffText = "\n ..."; + int maxDescLength = ar.DESCRIPTION.getDataType().length() - cutoffText.length(); + + if (fullDesc.length() > maxDescLength) { + String forcedCutoff = fullDesc.substring(0, maxDescLength); + return forcedCutoff + .substring(0, forcedCutoff.lastIndexOf("\n")) + .concat(cutoffText); + } else { + return fullDesc; + } + } + + + private static String toLink(EntityReference ref) { + String path = toPathSegment(ref.kind()); + return format( + "[%s](%s/%d)", + ref.name().orElse("??"), + path, + ref.id()); + } + + + private static String toPathSegment(EntityKind kind) { + switch (kind) { + case ACTOR: + return "actor"; + case APPLICATION: + return "application"; + case CHANGE_INITIATIVE: + return "change-initiative"; + case DATA_TYPE: + return "data-types"; + case FLOW_CLASSIFICATION_RULE: + return "flow-classification-rule"; + case LICENCE: + return "licence"; + case LOGICAL_DATA_FLOW: + return "logical-flow"; + case MEASURABLE: + return "measurable"; + case MEASURABLE_RATING: + return "measurable-rating"; + case PHYSICAL_FLOW: + return "physical-flow"; + case PHYSICAL_SPECIFICATION: + return "physical-specification"; + default: + throw new IllegalArgumentException(format("Cannot convert kind: %s to a path segment", kind)); + } + } + + + public Set findRippleConfig() { + Map configEntries = settingsDao + .indexByPrefix("job.RIPPLE_ASSESSMENTS."); + + return configEntries + .entrySet() + .stream() + .map(kv -> { + String key = kv.getKey(); + String value = kv.getValue(); + String rippleName = key.replaceAll("^job.RIPPLE_ASSESSMENTS.", ""); + LOG.debug("Parsing config ripple : {} , json: {}", rippleName, value); + + try { + return parseConfig(rippleName, value); + } catch (JsonProcessingException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(toSet()); + } +} diff --git a/waltz-data/src/test/java/org/finos/waltz/data/assessment_rating/AssessmentRatingRipplerTest.java b/waltz-data/src/test/java/org/finos/waltz/data/assessment_rating/AssessmentRatingRipplerTest.java new file mode 100644 index 0000000000..6e41c64463 --- /dev/null +++ b/waltz-data/src/test/java/org/finos/waltz/data/assessment_rating/AssessmentRatingRipplerTest.java @@ -0,0 +1,27 @@ +package org.finos.waltz.data.assessment_rating; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobConfiguration; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobStep; +import org.junit.jupiter.api.Test; + +import static org.finos.waltz.common.CollectionUtilities.first; +import static org.junit.jupiter.api.Assertions.*; + +class AssessmentRatingRipplerTest { + + + @Test + public void testSettingsParse() throws JsonProcessingException { + String value = "[ { \"from\" : \"FROM_DEF\", \"to\" : \"TO_DEF\" } ]"; + AssessmentRipplerJobConfiguration config = AssessmentRatingRippler.parseConfig("demoRippler", value); + + assertEquals("demoRippler", config.name()); + assertEquals(1, config.steps().size()); + + AssessmentRipplerJobStep step = first(config.steps()); + assertEquals("FROM_DEF", step.fromDef()); + assertEquals("TO_DEF", step.toDef()); + } + +} \ No newline at end of file diff --git a/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/BaseInMemoryIntegrationTest.java b/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/BaseInMemoryIntegrationTest.java index acc4d144ca..dfbea3f9bc 100644 --- a/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/BaseInMemoryIntegrationTest.java +++ b/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/BaseInMemoryIntegrationTest.java @@ -79,7 +79,7 @@ public void baseSetup() { } - private DSLContext getDsl() { + protected DSLContext getDsl() { return ctx.getBean(DSLContext.class); } diff --git a/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/service/AssessmentRipplerTest.java b/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/service/AssessmentRipplerTest.java new file mode 100644 index 0000000000..99d599d829 --- /dev/null +++ b/waltz-integration-test/src/test/java/org/finos/waltz/integration_test/inmem/service/AssessmentRipplerTest.java @@ -0,0 +1,183 @@ +package org.finos.waltz.integration_test.inmem.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.finos.waltz.data.assessment_rating.AssessmentRatingRippler; +import org.finos.waltz.data.settings.SettingsDao; +import org.finos.waltz.integration_test.inmem.BaseInMemoryIntegrationTest; +import org.finos.waltz.model.EntityKind; +import org.finos.waltz.model.EntityReference; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobStep; +import org.finos.waltz.model.assessment_definition.AssessmentVisibility; +import org.finos.waltz.model.assessment_definition.ImmutableAssessmentRipplerJobStep; +import org.finos.waltz.model.settings.ImmutableSetting; +import org.finos.waltz.model.settings.Setting; +import org.finos.waltz.schema.tables.AssessmentRating; +import org.finos.waltz.test_common.helpers.AppHelper; +import org.finos.waltz.test_common.helpers.AssessmentHelper; +import org.finos.waltz.test_common.helpers.MeasurableHelper; +import org.finos.waltz.test_common.helpers.RatingSchemeHelper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.finos.waltz.common.JacksonUtilities.getJsonMapper; +import static org.finos.waltz.common.ListUtilities.asList; +import static org.finos.waltz.model.EntityReference.mkRef; +import static org.finos.waltz.schema.Tables.ASSESSMENT_RATING; +import static org.finos.waltz.test_common.helpers.NameHelper.mkName; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AssessmentRipplerTest extends BaseInMemoryIntegrationTest { + + private static final AssessmentRating ar = ASSESSMENT_RATING; + + @Autowired + private AppHelper appHelper; + + @Autowired + private MeasurableHelper measurableHelper; + + @Autowired + private AssessmentHelper assessmentHelper; + + @Autowired + private RatingSchemeHelper ratingSchemeHelper; + + @Autowired + private SettingsDao settingsDao; + + @Autowired + private AssessmentRatingRippler assessmentRatingRippler; + + private final String stem = "ripple"; + + + @Test + public void cannotRippleBetweenAssessmentsWithDifferingRatingSchemes() { + long schemeA = ratingSchemeHelper.createEmptyRatingScheme(mkName(stem, "bad_ripple_a")); + long schemeB = ratingSchemeHelper.createEmptyRatingScheme(mkName(stem, "bad_ripple_b")); + + long assmtA = assessmentHelper.createDefinition(schemeA, mkName(stem, "ripple assmt A"), null, AssessmentVisibility.SECONDARY, stem); + long assmtB = assessmentHelper.createDefinition(schemeB, mkName(stem, "ripple assmt B"), null, AssessmentVisibility.SECONDARY, stem); + String assmtA_extId = mkName(stem, "ASSMT_A"); + String assmtB_extId = mkName(stem, "ASSMT_B"); + assessmentHelper.setDefExtId(assmtA, assmtA_extId); + assessmentHelper.setDefExtId(assmtB, assmtB_extId); + + assertThrows( + IllegalArgumentException.class, + () -> AssessmentRatingRippler.rippleAssessment( + getDsl(), + "admin", + "rippler_test", + assmtA_extId, + assmtB_extId)); + } + + + @Test + public void canRippleBetweenAssessmentsWithSameRatingSchemes() throws JsonProcessingException { + // setup app, measurable, and rating + EntityReference appRef = appHelper.createNewApp(mkName(stem, "ripple_app"), ouIds.root); + long categoryId = measurableHelper.createMeasurableCategory(mkName(stem, "ripple_mc")); + long measurableId = measurableHelper.createMeasurable(mkName(stem, "ripple_m"), categoryId); + + // link app to measurable + measurableHelper.createRating(appRef, measurableId); + + // create schemes, rating items and assessments + long scheme = ratingSchemeHelper.createEmptyRatingScheme(mkName(stem, "good_ripple_scheme")); + Long rsiId = ratingSchemeHelper.saveRatingItem(scheme, mkName(stem, "ripple_rsi"), 0, "pink", "P"); + long assmtA = assessmentHelper.createDefinition(scheme, mkName(stem, "ripple assmt A"), null, AssessmentVisibility.SECONDARY, stem, EntityKind.MEASURABLE, mkRef(EntityKind.MEASURABLE_CATEGORY, categoryId)); + long assmtB = assessmentHelper.createDefinition(scheme, mkName(stem, "ripple assmt B"), null, AssessmentVisibility.SECONDARY, stem, EntityKind.APPLICATION, null); + String assmtA_extId = mkName(stem, "ASSMT_A"); + String assmtB_extId = mkName(stem, "ASSMT_B"); + assessmentHelper.setDefExtId(assmtA, assmtA_extId); + assessmentHelper.setDefExtId(assmtB, assmtB_extId); + + // link assessment rating to measurable + assessmentHelper.createAssessment(assmtA, mkRef(EntityKind.MEASURABLE, measurableId), rsiId); + + AssessmentRipplerJobStep rippleStep = ImmutableAssessmentRipplerJobStep + .builder() + .fromDef(assmtA_extId) + .toDef(assmtB_extId) + .build(); + Setting rippleSetting = ImmutableSetting + .builder() + .name("job.RIPPLE_ASSESSMENTS."+mkName(stem, "rippleConfig")) + .value(getJsonMapper().writeValueAsString(asList(rippleStep))) + .build(); + settingsDao.create(rippleSetting); + + // ripple + assessmentRatingRippler.rippleAssessments(); + + // verify + assertEquals( + rsiId, + fetchAssessmentRatingItemId(appRef, assmtB), + "Rating will have rippled from measurable to application"); + } + + + @Test + public void canRippleUsingTheSettingsTableDefinitions() { + // setup app, measurable, and rating + EntityReference appRef = appHelper.createNewApp(mkName(stem, "ripple_app"), ouIds.root); + EntityReference unrelatedRef = appHelper.createNewApp(mkName(stem, "ripple_unrelated_app"), ouIds.root); + long categoryId = measurableHelper.createMeasurableCategory(mkName(stem, "ripple_mc")); + long measurableId = measurableHelper.createMeasurable(mkName(stem, "ripple_m"), categoryId); + + // link app to measurable + measurableHelper.createRating(appRef, measurableId); + + // create schemes, rating items and assessments + long scheme = ratingSchemeHelper.createEmptyRatingScheme(mkName(stem, "good_ripple_scheme")); + Long rsiId = ratingSchemeHelper.saveRatingItem(scheme, mkName(stem, "ripple_rsi"), 0, "pink", "P"); + long assmtA = assessmentHelper.createDefinition(scheme, mkName(stem, "ripple assmt A"), null, AssessmentVisibility.SECONDARY, stem, EntityKind.MEASURABLE, mkRef(EntityKind.MEASURABLE_CATEGORY, categoryId)); + long assmtB = assessmentHelper.createDefinition(scheme, mkName(stem, "ripple assmt B"), null, AssessmentVisibility.SECONDARY, stem, EntityKind.APPLICATION, null); + String assmtA_extId = mkName(stem, "ASSMT_A"); + String assmtB_extId = mkName(stem, "ASSMT_B"); + assessmentHelper.setDefExtId(assmtA, assmtA_extId); + assessmentHelper.setDefExtId(assmtB, assmtB_extId); + + // link assessment rating to measurable + assessmentHelper.createAssessment(assmtA, mkRef(EntityKind.MEASURABLE, measurableId), rsiId); + + // ripple + AssessmentRatingRippler.rippleAssessment( + getDsl(), + "admin", + "rippler_test", + assmtA_extId, + assmtB_extId); + + // verify + assertEquals( + rsiId, + fetchAssessmentRatingItemId(appRef, assmtB), + "Rating will have rippled from measurable to application"); + + assertNull( + fetchAssessmentRatingItemId(unrelatedRef, assmtB), + "Rating won't have rippled to an unrelated app"); + } + + + // --- helpers ------------- + + private Long fetchAssessmentRatingItemId(EntityReference ref, + long assessmentDefinitionId) { + return getDsl() + .select(ar.RATING_ID) + .from(ar) + .where(ar.ENTITY_KIND.eq(ref.kind().name())) + .and(ar.ENTITY_ID.eq(ref.id())) + .and(ar.ASSESSMENT_DEFINITION_ID.eq(assessmentDefinitionId)) + .fetchOne(ar.RATING_ID); + } + + +} diff --git a/waltz-model/src/main/java/org/finos/waltz/model/PairDiffResult.java b/waltz-model/src/main/java/org/finos/waltz/model/PairDiffResult.java new file mode 100644 index 0000000000..0794a5e963 --- /dev/null +++ b/waltz-model/src/main/java/org/finos/waltz/model/PairDiffResult.java @@ -0,0 +1,71 @@ +package org.finos.waltz.model; + +import org.immutables.value.Value; +import org.jooq.lambda.tuple.Tuple2; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.finos.waltz.common.MapUtilities.indexBy; +import static org.jooq.lambda.tuple.Tuple.tuple; + +@Value.Immutable +public abstract class PairDiffResult { + + public abstract Collection> allIntersection(); + public abstract Collection otherOnly(); + public abstract Collection waltzOnly(); + public abstract Collection> differingIntersection(); + + + public static PairDiffResult mkPairDiff(Collection waltzRecords, + Collection otherRecords, + Function aKeyFn, + Function bKeyFn, + BiPredicate equalityPredicate) { + Map waltzByKey = indexBy(waltzRecords, aKeyFn); + Map othersByKey = indexBy(otherRecords, bKeyFn); + + Set otherOnly = otherRecords.stream() + .filter(f -> !waltzByKey.containsKey(bKeyFn.apply(f))) + .collect(Collectors.toSet()); + + Set waltzOnly = waltzRecords.stream() + .filter(f -> !othersByKey.containsKey(aKeyFn.apply(f))) + .collect(Collectors.toSet()); + + Set> intersect = otherRecords.stream() + .map(other -> tuple( + waltzByKey.get(bKeyFn.apply(other)), + other)) + .filter(t -> t.v1 != null) + .collect(Collectors.toSet()); + + Set> differingIntersection = intersect.stream() + .filter(t -> ! equalityPredicate.test(t.v1, t.v2)) + .collect(Collectors.toSet()); + + return ImmutablePairDiffResult + .builder() + .otherOnly(otherOnly) + .waltzOnly(waltzOnly) + .allIntersection(intersect) + .differingIntersection(differingIntersection) + .build(); + } + + @Override + public String toString() { + return String.format( + "%s - [Intersection: %s records, Other: %s records, Waltz: %s records, Differing Intersection: %s records]", + getClass().getName(), + allIntersection().size(), + otherOnly().size(), + waltzOnly().size(), + differingIntersection().size()); + } +} diff --git a/waltz-model/src/main/java/org/finos/waltz/model/assessment_definition/AssessmentRipplerJobConfiguration.java b/waltz-model/src/main/java/org/finos/waltz/model/assessment_definition/AssessmentRipplerJobConfiguration.java new file mode 100644 index 0000000000..7d9d177e4d --- /dev/null +++ b/waltz-model/src/main/java/org/finos/waltz/model/assessment_definition/AssessmentRipplerJobConfiguration.java @@ -0,0 +1,18 @@ +package org.finos.waltz.model.assessment_definition; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value; + +import java.util.List; + +@Value.Immutable +@JsonDeserialize(as = ImmutableAssessmentRipplerJobConfiguration.class) +@JsonSerialize(as = ImmutableAssessmentRipplerJobConfiguration.class) +public interface AssessmentRipplerJobConfiguration { + + String name(); + + List steps(); + +} diff --git a/waltz-model/src/main/java/org/finos/waltz/model/assessment_definition/AssessmentRipplerJobStep.java b/waltz-model/src/main/java/org/finos/waltz/model/assessment_definition/AssessmentRipplerJobStep.java new file mode 100644 index 0000000000..dd85923198 --- /dev/null +++ b/waltz-model/src/main/java/org/finos/waltz/model/assessment_definition/AssessmentRipplerJobStep.java @@ -0,0 +1,19 @@ +package org.finos.waltz.model.assessment_definition; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value; + +@Value.Immutable +@JsonDeserialize(as = ImmutableAssessmentRipplerJobStep.class) +@JsonSerialize(as = ImmutableAssessmentRipplerJobStep.class) +public interface AssessmentRipplerJobStep { + + @JsonAlias({"from", "from_def"}) + String fromDef(); + + @JsonAlias({"to", "to_def"}) + String toDef(); + +} diff --git a/waltz-model/src/main/java/org/finos/waltz/model/scheduled_job/JobKey.java b/waltz-model/src/main/java/org/finos/waltz/model/scheduled_job/JobKey.java index 4fe47b44af..3dd77ce19c 100644 --- a/waltz-model/src/main/java/org/finos/waltz/model/scheduled_job/JobKey.java +++ b/waltz-model/src/main/java/org/finos/waltz/model/scheduled_job/JobKey.java @@ -39,5 +39,6 @@ public enum JobKey { REPORT_GRID_RECALCULATE_APP_GROUPS_FROM_FILTERS, ALLOCATED_COSTS_POPULATOR, + RIPPLE_ASSESSMENTS, COMPLEXITY_REBUILD_MEASURABLE } diff --git a/waltz-ng/client/assessments/services/assessment-rating-store.js b/waltz-ng/client/assessments/services/assessment-rating-store.js index adb967c2df..859bdc9b3a 100644 --- a/waltz-ng/client/assessments/services/assessment-rating-store.js +++ b/waltz-ng/client/assessments/services/assessment-rating-store.js @@ -95,6 +95,18 @@ export function store($http, BaseApiUrl) { .then(d => d.data); }; + const ripple = () => { + return $http + .post(`${BASE}/ripple/all`) + .then(d => d.data); + }; + + const findRippleConfig = () => { + return $http + .get(`${BASE}/ripple/config`) + .then(d => d.data); + }; + return { getRatingPermissions, findForEntityReference, @@ -106,7 +118,9 @@ export function store($http, BaseApiUrl) { unlock, bulkStore, bulkRemove, - remove + remove, + ripple, + findRippleConfig }; } @@ -175,6 +189,16 @@ export const AssessmentRatingStore_API = { serviceName, serviceFnName: "remove", description: "remove a rating" + }, + ripple: { + serviceName, + serviceFnName: "ripple", + description: "ripple assessments based on the settings table config" + }, + findRippleConfig: { + serviceName, + serviceFnName: "findRippleConfig", + description: "ripple assessments based on the settings table config" } }; diff --git a/waltz-ng/client/system/recalculate-view.html b/waltz-ng/client/system/recalculate-view.html index 2a6c2df969..52eb6b3f01 100644 --- a/waltz-ng/client/system/recalculate-view.html +++ b/waltz-ng/client/system/recalculate-view.html @@ -42,7 +42,7 @@

-

Rated Flows

+

Rated Flows

Logical flows are decorated with data types which may in turn be rated against Authoritative Sources. Use the link @@ -50,14 +50,15 @@

Rated Flows

+

-

Data Type Usages

+

Data Type Usages

Applications track what data types they use and how they get used. Use the link below to recalculate all @@ -65,11 +66,140 @@

Data Type Usages

+ +
+
+

Assessment Rippler

+

+ The assessment rippler can copy assessment ratings between different definitions + providing they share the same rating scheme. For example, you may have a critical process + assessment on a measurable and want an equivalent criticality flag to be reflected on + all applications which are aligned to that measurable. +

+
+ Configuration + +

Active Configuration

+
+ +

+ Each row in the table below represents a step in the flow, rippling assessments + between a source (From) and a target (To). + If there are multiple steps they are applied in the sequence shown below. +

+ + + + + + + + + + + + + + + + + + + +
From AssessmentFrom Entity KindTo AssessmentTo Entity Kind
+ + () + + + + » + + + () + + +
+
+ +
+ +

Setup

+

+ To configure the assessment rippler you need to create an entry in the + settings + table for each 'ripple-flow'. For example: +

+ + + + + + + + + + + + + + + + + + + + +
NameDescriptionExample
Setting key + The key must be of the form: + job.RIPPLE_ASSESSMENTS.??? + , the ??? + should be a unique name to identify the ripple flow + + job.RIPPLE_ASSESSMENTS.nice_name +
Setting value + A list of json objects, each object represents the 'from' and 'to' + external id's of the assessments to be rippled + + [{"from": "def1_ext_id", "to": "def2_ext_id"}] +
+

+ To trigger this action (i.e. from a scheduled job), you can add/udpate an + entry in the settings table: + +

+ + + + + + + + + + + + + +
Setting KeySetting Value
+ RIPPLE_ASSESSMENTS + + RUNNABLE +
+ +
+ + +
+ diff --git a/waltz-ng/client/system/recalculate-view.js b/waltz-ng/client/system/recalculate-view.js index cdf74774d3..d19f975a14 100644 --- a/waltz-ng/client/system/recalculate-view.js +++ b/waltz-ng/client/system/recalculate-view.js @@ -19,10 +19,41 @@ import template from "./recalculate-view.html"; import {CORE_API} from "../common/services/core-api-utils"; import toasts from "../svelte-stores/toast-store"; +import {initialiseData} from "../common"; +import _ from "lodash"; +const initialState = { + assessmentRippleConfig: [] +}; + + +function controller($q, serviceBroker) { + const vm = initialiseData(this, initialState); + + vm.$onInit = () => { + const configPromise = serviceBroker + .loadViewData(CORE_API.AssessmentRatingStore.findRippleConfig); + + const defsPromise = serviceBroker + .loadViewData(CORE_API.AssessmentDefinitionStore.findAll); -function controller(serviceBroker) { - const vm = this; + $q.all([configPromise, defsPromise]) + .then(([configResponse, defsResponse]) => { + const defsByExtId = _.keyBy( + defsResponse.data, + d => d.externalId); + + vm.assessmentRippleConfig = _ + .chain(configResponse.data) + .map(d => { + const steps = _.map( + d.steps, + s => ({from: defsByExtId[s.fromDef], to: defsByExtId[s.toDef]})); + return Object.assign({}, d, {steps}); + }) + .value(); + }); + }; vm.recalcFlowRatings = () => { toasts.info("Flow Ratings recalculation requested"); @@ -38,10 +69,17 @@ function controller(serviceBroker) { .then(() => toasts.success("Data Type Usage recalculated")); }; + vm.rippleAssessments = () => { + toasts.info("Assessment Ripple requested"); + serviceBroker + .execute(CORE_API.AssessmentRatingStore.ripple) + .then(r => toasts.success(`Assessment Ripple finished. Completed ${r.data} step/s`)); + }; } controller.$inject = [ + "$q", "ServiceBroker" ]; @@ -49,7 +87,7 @@ controller.$inject = [ const page = { template, controller, - controllerAs: "ctrl" + controllerAs: "$ctrl" }; diff --git a/waltz-ng/client/system/system-admin-list.js b/waltz-ng/client/system/system-admin-list.js index ad726839a1..8ffd4dd710 100644 --- a/waltz-ng/client/system/system-admin-list.js +++ b/waltz-ng/client/system/system-admin-list.js @@ -191,7 +191,7 @@ const maintenanceOptions= [ }, { name: "Recalculate Derived Fields", role: "ADMIN", - description: "Recompute: Data Type Usages, Flow Authoritative Source Ratings", + description: "Recompute: Data Type Usages, Flow Authoritative Source Ratings, Ripple Assessments", state: "main.system.recalculate", icon: "calculator" }, { diff --git a/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java b/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java index 9f78556c3d..6a48b1bd4c 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java @@ -24,12 +24,28 @@ import org.finos.waltz.data.GenericSelectorFactory; import org.finos.waltz.data.assessment_definition.AssessmentDefinitionDao; import org.finos.waltz.data.assessment_rating.AssessmentRatingDao; +import org.finos.waltz.data.assessment_rating.AssessmentRatingRippler; import org.finos.waltz.data.rating_scheme.RatingSchemeDAO; -import org.finos.waltz.model.*; +import org.finos.waltz.model.Cardinality; +import org.finos.waltz.model.EntityKind; +import org.finos.waltz.model.EntityReference; +import org.finos.waltz.model.IdSelectionOptions; +import org.finos.waltz.model.NameProvider; +import org.finos.waltz.model.Operation; +import org.finos.waltz.model.Severity; +import org.finos.waltz.model.UserTimestamp; import org.finos.waltz.model.application.AssessmentsView; import org.finos.waltz.model.application.ImmutableAssessmentsView; import org.finos.waltz.model.assessment_definition.AssessmentDefinition; -import org.finos.waltz.model.assessment_rating.*; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobConfiguration; +import org.finos.waltz.model.assessment_rating.AssessmentDefinitionRatingOperations; +import org.finos.waltz.model.assessment_rating.AssessmentRating; +import org.finos.waltz.model.assessment_rating.AssessmentRatingSummaryCounts; +import org.finos.waltz.model.assessment_rating.BulkAssessmentRatingCommand; +import org.finos.waltz.model.assessment_rating.ImmutableAssessmentRating; +import org.finos.waltz.model.assessment_rating.RemoveAssessmentRatingCommand; +import org.finos.waltz.model.assessment_rating.SaveAssessmentRatingCommand; +import org.finos.waltz.model.assessment_rating.UpdateRatingCommand; import org.finos.waltz.model.changelog.ChangeLog; import org.finos.waltz.model.changelog.ImmutableChangeLog; import org.finos.waltz.model.rating.RatingScheme; @@ -39,7 +55,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static java.lang.String.format; @@ -59,6 +79,7 @@ public class AssessmentRatingService { private final ChangeLogService changeLogService; private final AssessmentRatingPermissionChecker assessmentRatingPermissionChecker; private final GenericSelectorFactory genericSelectorFactory = new GenericSelectorFactory(); + private final AssessmentRatingRippler rippler; @Autowired @@ -67,19 +88,22 @@ public AssessmentRatingService( AssessmentDefinitionDao assessmentDefinitionDao, RatingSchemeDAO ratingSchemeDAO, ChangeLogService changeLogService, - AssessmentRatingPermissionChecker assessmentRatingPermissionChecker) { + AssessmentRatingPermissionChecker assessmentRatingPermissionChecker, + AssessmentRatingRippler rippler) { checkNotNull(assessmentRatingDao, "assessmentRatingDao cannot be null"); checkNotNull(assessmentDefinitionDao, "assessmentDefinitionDao cannot be null"); checkNotNull(ratingSchemeDAO, "ratingSchemeDao cannot be null"); checkNotNull(assessmentRatingPermissionChecker, "ratingPermissionChecker cannot be null"); checkNotNull(changeLogService, "changeLogService cannot be null"); + checkNotNull(rippler, "rippler cannot be null"); this.assessmentRatingPermissionChecker = assessmentRatingPermissionChecker; this.assessmentRatingDao = assessmentRatingDao; this.ratingSchemeDAO = ratingSchemeDAO; this.assessmentDefinitionDao = assessmentDefinitionDao; this.changeLogService = changeLogService; + this.rippler = rippler; } @@ -477,4 +501,12 @@ public AssessmentsView getPrimaryAssessmentsViewForKindAndSelector(EntityKind en .ratingSchemeItems(assessmentRatingSchemeItems) .build(); } + + public Long rippleAll() { + return rippler.rippleAssessments(); + } + + public Set findRippleConfig() { + return rippler.findRippleConfig(); + } } \ No newline at end of file diff --git a/waltz-service/src/main/java/org/finos/waltz/service/scheduled_job/ScheduledJobService.java b/waltz-service/src/main/java/org/finos/waltz/service/scheduled_job/ScheduledJobService.java index 6caa2f571a..1ee8c09bc1 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/scheduled_job/ScheduledJobService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/scheduled_job/ScheduledJobService.java @@ -20,6 +20,7 @@ import org.finos.waltz.common.ExcludeFromIntegrationTesting; +import org.finos.waltz.data.assessment_rating.AssessmentRatingRippler; import org.finos.waltz.data.scheduled_job.ScheduledJobDao; import org.finos.waltz.model.EntityKind; import org.finos.waltz.model.scheduled_job.JobKey; @@ -67,6 +68,7 @@ public class ScheduledJobService { private final CostService costService; private final SurveyInstanceActionQueueService surveyInstanceActionQueueService; private final ComplexityService complexityService; + private final AssessmentRatingRippler assessmentRatingRippler; @Autowired @@ -81,7 +83,8 @@ public ScheduledJobService(AttestationRunService attestationRunService, ReportGridFilterViewService reportGridFilterViewService, ScheduledJobDao scheduledJobDao, SurveyInstanceActionQueueService surveyInstanceActionQueueService, - SurveyInstanceService surveyInstanceService) { + SurveyInstanceService surveyInstanceService, + AssessmentRatingRippler assessmentRatingRippler) { checkNotNull(attestationRunService, "attestationRunService cannot be null"); @@ -95,6 +98,7 @@ public ScheduledJobService(AttestationRunService attestationRunService, checkNotNull(scheduledJobDao, "scheduledJobDao cannot be null"); checkNotNull(surveyInstanceActionQueueService, "surveyInstanceActionQueueService cannot be null"); checkNotNull(surveyInstanceService, "surveyInstanceService cannot be null"); + checkNotNull(assessmentRatingRippler, "assessmentRatingRippler cannot be null"); this.attestationRunService = attestationRunService; this.complexityService = complexityService; @@ -108,6 +112,7 @@ public ScheduledJobService(AttestationRunService attestationRunService, this.scheduledJobDao = scheduledJobDao; this.surveyInstanceActionQueueService = surveyInstanceActionQueueService; this.surveyInstanceService = surveyInstanceService; + this.assessmentRatingRippler = assessmentRatingRippler; } @@ -164,6 +169,9 @@ public void run() { runIfNeeded(JobKey.COMPLEXITY_REBUILD_MEASURABLE, (jk) -> complexityService.populateMeasurableComplexities()); + runIfNeeded(JobKey.RIPPLE_ASSESSMENTS, + (jk) -> assessmentRatingRippler.rippleAssessments()); + surveyInstanceActionQueueService.performActions(); } diff --git a/waltz-test-common/src/main/java/org/finos/waltz/test_common/helpers/AssessmentHelper.java b/waltz-test-common/src/main/java/org/finos/waltz/test_common/helpers/AssessmentHelper.java index 9cd8858773..f5a41435de 100644 --- a/waltz-test-common/src/main/java/org/finos/waltz/test_common/helpers/AssessmentHelper.java +++ b/waltz-test-common/src/main/java/org/finos/waltz/test_common/helpers/AssessmentHelper.java @@ -87,7 +87,6 @@ public void createAssessment(Long defId, EntityReference ref, Long ratingId, Str public void createAssessment(Long defId, EntityReference ref, Long ratingId) { - AssessmentRatingRecord record = dsl.newRecord(ASSESSMENT_RATING); record.setAssessmentDefinitionId(defId); record.setEntityId(ref.id()); @@ -106,6 +105,7 @@ public void createAssessment(Long defId, EntityReference ref, Long ratingId) { .execute(); } + public void updateDefinitionReadOnly(long defnId) { dsl .update(ASSESSMENT_DEFINITION) @@ -114,6 +114,7 @@ public void updateDefinitionReadOnly(long defnId) { .execute(); } + public void updateRatingReadOnly(EntityReference ref, long defnId) { dsl .update(ASSESSMENT_RATING) @@ -124,4 +125,12 @@ public void updateRatingReadOnly(EntityReference ref, long defnId) { .execute(); } + + public void setDefExtId(long assessmentDefinitionId, + String externalId) { + dsl.update(ASSESSMENT_DEFINITION) + .set(ASSESSMENT_DEFINITION.EXTERNAL_ID, externalId) + .where(ASSESSMENT_DEFINITION.ID.eq(assessmentDefinitionId)) + .execute(); + } } diff --git a/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/AssessmentRatingEndpoint.java b/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/AssessmentRatingEndpoint.java index cba2cdf385..e69f1c1ca8 100644 --- a/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/AssessmentRatingEndpoint.java +++ b/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/AssessmentRatingEndpoint.java @@ -23,7 +23,9 @@ import org.finos.waltz.model.EntityKind; import org.finos.waltz.model.UserTimestamp; import org.finos.waltz.model.assessment_definition.AssessmentDefinition; +import org.finos.waltz.model.assessment_definition.AssessmentRipplerJobConfiguration; import org.finos.waltz.model.assessment_rating.*; +import org.finos.waltz.model.user.SystemRole; import org.finos.waltz.service.assessment_definition.AssessmentDefinitionService; import org.finos.waltz.service.assessment_rating.AssessmentRatingService; import org.finos.waltz.service.permission.permission_checker.AssessmentRatingPermissionChecker; @@ -92,6 +94,8 @@ public void register() { String findRatingPermissionsPath = mkPath(BASE_URL, "entity", ":kind", ":id", ":assessmentDefinitionId", "permissions"); String bulkUpdatePath = mkPath(BASE_URL, "bulk-update", ":assessmentDefinitionId"); String bulkRemovePath = mkPath(BASE_URL, "bulk-remove", ":assessmentDefinitionId"); + String rippleAllPath = mkPath(BASE_URL, "ripple", "all"); + String rippleConfigPath = mkPath(BASE_URL, "ripple", "config"); getForList(findForEntityPath, this::findForEntityRoute); getForList(findByEntityKindPath, this::findByEntityKindRoute); @@ -108,6 +112,8 @@ public void register() { putForDatum(lockPath, this::lockRoute); putForDatum(unlockPath, this::unlockRoute); deleteForDatum(removePath, this::removeRoute); + postForDatum(rippleAllPath, this::rippleRoute); + getForList(rippleConfigPath, this::rippleConfigRoute); } @@ -215,6 +221,17 @@ private boolean bulkRemoveRoute(Request request, Response z) throws IOException } + private Long rippleRoute(Request request, Response z) { + requireRole(userRoleService, request, SystemRole.ADMIN); + return assessmentRatingService.rippleAll(); + } + + + private Set rippleConfigRoute(Request request, Response z) { + return assessmentRatingService.findRippleConfig(); + } + + private boolean removeRoute(Request request, Response z) throws InsufficientPrivelegeException { String username = getUsername(request); UserTimestamp lastUpdate = UserTimestamp.mkForUser(username);