From dc5719d29877fa6fa91d661462ea4a08ad88edec Mon Sep 17 00:00:00 2001 From: David Watkins Date: Fri, 9 Jun 2023 15:10:16 +0100 Subject: [PATCH] Primary rating #6635 --- .../MeasurableIdSelectorFactory.java | 10 ++++ .../MeasurableRatingDao.java | 43 +++++++++++++++-- .../measurable_rating/MeasurableRating.java | 4 ++ .../SaveMeasurableRatingCommand.java | 4 ++ .../measurable-rating-edit-panel.html | 40 ++++++++++++---- .../measurable-rating-edit-panel.js | 11 +++-- .../tree/measurable-rating-tree.html | 11 +++++ .../services/measurable-rating-store.js | 4 +- .../resources/liquibase/db.changelog-1.51.xml | 25 ++++++++++ .../MeasurableRatingService.java | 46 ++++++++++++------- .../api/MeasurableRatingEndpoint.java | 25 +++++++++- 11 files changed, 186 insertions(+), 37 deletions(-) diff --git a/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableIdSelectorFactory.java b/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableIdSelectorFactory.java index 80345e7f0..032d30450 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableIdSelectorFactory.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableIdSelectorFactory.java @@ -47,6 +47,16 @@ public class MeasurableIdSelectorFactory implements IdSelectorFactory { private final OrganisationalUnitIdSelectorFactory orgUnitIdSelectorFactory = new OrganisationalUnitIdSelectorFactory(); + public static SelectConditionStep> allMeasurablesIdsInSameCategory(Long measurableId) { + return DSL + .select(MEASURABLE.ID) + .from(MEASURABLE) + .where(MEASURABLE.MEASURABLE_CATEGORY_ID.eq(DSL + .select(MEASURABLE.MEASURABLE_CATEGORY_ID) + .from(MEASURABLE) + .where(MEASURABLE.ID.eq(measurableId)))); + } + @Override public Select> apply(IdSelectionOptions options) { switch (options.entityReference().kind()) { diff --git a/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java b/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java index 8bc293877..161c3a691 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java @@ -58,6 +58,7 @@ import static org.finos.waltz.common.SetUtilities.union; import static org.finos.waltz.common.StringUtilities.firstChar; import static org.finos.waltz.common.StringUtilities.notEmpty; +import static org.finos.waltz.data.measurable.MeasurableIdSelectorFactory.allMeasurablesIdsInSameCategory; import static org.finos.waltz.schema.Tables.*; import static org.finos.waltz.schema.tables.Application.APPLICATION; import static org.finos.waltz.schema.tables.Measurable.MEASURABLE; @@ -103,6 +104,7 @@ public class MeasurableRatingDao { .lastUpdatedAt(toLocalDateTime(r.getLastUpdatedAt())) .lastUpdatedBy(r.getLastUpdatedBy()) .isReadOnly(r.getIsReadonly()) + .isPrimary(r.getIsPrimary()) .build(); }; @@ -129,6 +131,7 @@ public class MeasurableRatingDao { record.setLastUpdatedAt(Timestamp.valueOf(command.lastUpdate().at())); record.setLastUpdatedBy(command.lastUpdate().by()); record.setProvenance(command.provenance()); + record.setIsPrimary(command.isPrimary()); return record; }; @@ -157,6 +160,7 @@ public Operation save(SaveMeasurableRatingCommand command, boolean ignoreReadOnl .set(MEASURABLE_RATING.LAST_UPDATED_BY, command.lastUpdate().by()) .set(MEASURABLE_RATING.LAST_UPDATED_AT, command.lastUpdate().atTimestamp()) .set(MEASURABLE_RATING.PROVENANCE, command.provenance()) + .set(MEASURABLE_RATING.IS_PRIMARY, command.isPrimary()) .where(MEASURABLE_RATING.ENTITY_ID.eq(command.entityReference().id())) .and(MEASURABLE_RATING.ENTITY_KIND.eq(command.entityReference().kind().name())) .and(MEASURABLE_RATING.MEASURABLE_ID.eq(command.measurableId())) @@ -169,7 +173,7 @@ public Operation save(SaveMeasurableRatingCommand command, boolean ignoreReadOnl throw new NotFoundException( "MR_SAVE_UPDATE_FAILED", format("Could find writable associated record to update for rating: %s", command)); - }; + } return Operation.UPDATE; } else { if (dsl.executeInsert(record) != 1) { @@ -177,7 +181,7 @@ public Operation save(SaveMeasurableRatingCommand command, boolean ignoreReadOnl "MR_SAVE_INSERT_FAILED", format("Creation of record failed: %s", command)); } - ; + return Operation.ADD; } } @@ -334,6 +338,7 @@ private SelectJoinStep mkBaseQuery() { .from(MEASURABLE_RATING); } + public Set calculateAmendedRatingOperations(Set operationsForEntityAssessment, EntityReference entityReference, long measurableId, @@ -367,6 +372,7 @@ public Set calculateAmendedRatingOperations(Set operations } } + public Set calculateAmendedAllocationOperations(Set operationsForAllocation, long categoryId, String username) { @@ -384,9 +390,9 @@ public Set calculateAmendedAllocationOperations(Set operat } else { return operationsForAllocation; } - } + /** * Takes a source measurable and will move all ratings, decommission dates, replacement applications and allocations to the target measurable where possible. * If a value already exists on the target the migration is ignored, or in the case of allocations, aggregated. @@ -564,7 +570,10 @@ public void migrateRatings(Long measurableId, Long targetId, String userId) { targetId, EntityKind.ALLOCATION, Operation.UPDATE, - format("Removed %d ratings from measurable: %d where they could not be migrated due to an existing rating on the target", removedRatings, measurableId, targetId), + format("Removed %d ratings from measurable: %d where they could not be migrated due to an existing rating on the target (%d)", + removedRatings, + measurableId, + targetId), userId); } @@ -605,6 +614,7 @@ private SelectOrderByStep> selectRatingsThatCanBeModified( return migrations.except(targets); } + private SelectConditionStep> mkEntitySelectForMeasurable(Long measurableId) { return DSL .select(Tables.MEASURABLE_RATING.ENTITY_ID, Tables.MEASURABLE_RATING.ENTITY_KIND) @@ -612,6 +622,7 @@ private SelectConditionStep> mkEntitySelectForMeasurable(L .where(Tables.MEASURABLE_RATING.MEASURABLE_ID.eq(measurableId)); } + public int getSharedRatingsCount(Long measurableId, Long targetId) { SelectConditionStep> targets = mkEntitySelectForMeasurable(targetId); @@ -622,6 +633,7 @@ public int getSharedRatingsCount(Long measurableId, Long targetId) { return dsl.fetchCount(sharedRatings); } + public int getSharedDecommsCount(Long measurableId, Long targetId) { SelectConditionStep> targets = mkEntitySelectForDecomm(targetId); @@ -679,6 +691,7 @@ private SelectOrderByStep> selectAllocsToBeSummed(Lo return migrations.intersect(targets); } + private SelectHavingStep> selectAllocsToBeUpdated(Long measurableId, Long targetId) { SelectOrderByStep> valuesToBeSummed = selectAllocsToBeSummed(measurableId, targetId); @@ -696,4 +709,26 @@ private SelectHavingStep> selectAllocsToBeU .groupBy(ALLOCATION.ALLOCATION_SCHEME_ID, ALLOCATION.ENTITY_ID, ALLOCATION.ENTITY_KIND); } + /** + * Updates the given measurable rating to be set as primary. + * All other ratings for the same entity/category will be set to non-primary. + * + * @param tx the dsl connection to use + * @param entityId the entity id + * @param measurableId the measurbable id + */ + private void updateToPrimary(DSLContext tx, Long entityId, Long measurableId) { + tx.update(MEASURABLE_RATING) + .set(MEASURABLE_RATING.IS_PRIMARY, false) + .where(MEASURABLE_RATING.ENTITY_ID.eq(entityId)) + .and(MEASURABLE_RATING.MEASURABLE_ID.eq(allMeasurablesIdsInSameCategory(measurableId))) + .execute(); + + tx.update(MEASURABLE_RATING) + .set(MEASURABLE_RATING.IS_PRIMARY, true) + .where(MEASURABLE_RATING.ENTITY_ID.eq(entityId)) + .and(MEASURABLE_RATING.MEASURABLE_ID.eq(measurableId)) + .execute(); + } + } diff --git a/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/MeasurableRating.java b/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/MeasurableRating.java index 510c12975..69ccd5e4c 100644 --- a/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/MeasurableRating.java +++ b/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/MeasurableRating.java @@ -23,6 +23,7 @@ import org.finos.waltz.model.DescriptionProvider; import org.finos.waltz.model.EntityReference; import org.finos.waltz.model.LastUpdatedProvider; +import org.finos.waltz.model.Nullable; import org.finos.waltz.model.ProvenanceProvider; import org.immutables.value.Value; @@ -41,4 +42,7 @@ public abstract class MeasurableRating implements @Value.Default public boolean isReadOnly() { return false; } + + @Nullable + public abstract Boolean isPrimary(); } diff --git a/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/SaveMeasurableRatingCommand.java b/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/SaveMeasurableRatingCommand.java index 0c72897ea..2dab364d5 100644 --- a/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/SaveMeasurableRatingCommand.java +++ b/waltz-model/src/main/java/org/finos/waltz/model/measurable_rating/SaveMeasurableRatingCommand.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.finos.waltz.model.DescriptionProvider; +import org.finos.waltz.model.Nullable; import org.finos.waltz.model.ProvenanceProvider; import org.immutables.value.Value; @@ -34,5 +35,8 @@ public abstract class SaveMeasurableRatingCommand extends MeasurableRatingComman public abstract char rating(); public abstract Optional previousRating(); + @Nullable + public abstract Boolean isPrimary(); + } diff --git a/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.html b/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.html index 4ac3ecdda..b05a39246 100644 --- a/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.html +++ b/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.html @@ -36,16 +36,14 @@
- -
- - Locked - -

- This rating has been marked as read only and cannot be edited -

-
-
+
+ + Locked + +

+ This rating has been marked as read only and cannot be edited +

+
@@ -244,6 +242,28 @@
+
+
+ Primary: +
+
+ + +
+
+
+
+

+ Only one primary rating is allowed per category, selecting this will deselect any other primary ratings for this entity +

+
+
+
diff --git a/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.js b/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.js index 657f1167d..bbd5866f8 100644 --- a/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.js +++ b/waltz-ng/client/measurable-rating/components/edit-panel/measurable-rating-edit-panel.js @@ -114,15 +114,16 @@ function controller($q, }; const getDescription = () => _.get(vm.selected, ["rating", "description"], ""); + const getIsPrimary = () => _.get(vm.selected, ["rating", "isPrimary"], ""); const getRating = () => _.get(vm.selected, ["rating", "rating"]); - const doRatingSave = (rating, description) => { + const doRatingSave = (rating, description, isPrimary) => { const currentRating = !_.isEmpty(vm.selected.rating) ? vm.selected.rating.rating : null; return serviceBroker .execute( CORE_API.MeasurableRatingStore.save, - [vm.parentEntityRef, vm.selected.measurable.id, rating, currentRating, description]) + [vm.parentEntityRef, vm.selected.measurable.id, rating, currentRating, description, isPrimary]) .then(r => { vm.ratings = r.data }) .then(() => recalcTabs()) .then(() => { @@ -318,7 +319,7 @@ function controller($q, vm.saveInProgress = false; displayError("Could not remove measurable rating.", e); }) - : doRatingSave(r, getDescription()) + : doRatingSave(r, getDescription(), getIsPrimary()) .then(() => toasts.success(`Saved: ${vm.selected.measurable.name}`)) .catch(e => { deselectMeasurable(); @@ -358,6 +359,10 @@ function controller($q, } }; + vm.onPrimaryToggle = (d) => { + console.log("Toggling primary", d); + }; + vm.onTabChange = () => { deselectMeasurable(); diff --git a/waltz-ng/client/measurable-rating/components/tree/measurable-rating-tree.html b/waltz-ng/client/measurable-rating/components/tree/measurable-rating-tree.html index b9a5e17ea..3a92301ec 100644 --- a/waltz-ng/client/measurable-rating/components/tree/measurable-rating-tree.html +++ b/waltz-ng/client/measurable-rating/components/tree/measurable-rating-tree.html @@ -76,6 +76,9 @@ + + @@ -128,6 +131,14 @@
+
+ + + This feature has been marked as primary +
+
diff --git a/waltz-ng/client/measurable-rating/services/measurable-rating-store.js b/waltz-ng/client/measurable-rating/services/measurable-rating-store.js index 30a9f3ede..40bf00845 100644 --- a/waltz-ng/client/measurable-rating/services/measurable-rating-store.js +++ b/waltz-ng/client/measurable-rating/services/measurable-rating-store.js @@ -70,10 +70,10 @@ function store($http, baseApiUrl) { .then(d => d.data); }; - const save = (ref, measurableId, rating = "Z", previousRating, description = "") => { + const save = (ref, measurableId, rating = "Z", previousRating, description = "", isPrimary = false) => { checkIsEntityRef(ref); return $http - .post(`${baseUrl}/entity/${ref.kind}/${ref.id}/measurable/${measurableId}`, { rating, previousRating, description }) + .post(`${baseUrl}/entity/${ref.kind}/${ref.id}/measurable/${measurableId}`, { rating, previousRating, description, isPrimary }) .then(d => d.data); }; diff --git a/waltz-schema/src/main/resources/liquibase/db.changelog-1.51.xml b/waltz-schema/src/main/resources/liquibase/db.changelog-1.51.xml index c6a312a18..c39ab26ab 100644 --- a/waltz-schema/src/main/resources/liquibase/db.changelog-1.51.xml +++ b/waltz-schema/src/main/resources/liquibase/db.changelog-1.51.xml @@ -195,4 +195,29 @@ columnName="involvement_kind_ids"/> + + + + + + + + + + + + + + + + + + + + + + diff --git a/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java b/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java index b303a71b5..d1d3018b5 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java @@ -268,22 +268,6 @@ private String getEntityName(MeasurableRatingCommand command) { } - private void checkRatingIsAllowable(SaveMeasurableRatingCommand command) { - - long measurableCategory = measurableDao.getById(command.measurableId()).categoryId(); - EntityReference entityReference = command.entityReference(); - - Boolean isRestricted = ratingSchemeService - .findRatingSchemeItemsForEntityAndCategory(entityReference, measurableCategory) - .stream() - .filter(r -> r.rating().equals(command.rating())) - .map(RatingSchemeItem::isRestricted) - .findFirst() - .orElse(false); - - checkFalse(isRestricted, "New rating is restricted, rating not saved"); - } - public boolean checkRatingExists(SaveMeasurableRatingCommand command) { return measurableRatingDao.checkRatingExists(command); @@ -300,4 +284,34 @@ public int getSharedRatingsCount(Long measurableId, Long targetMeasurableId) { public int getSharedDecommsCount(Long measurableId, Long targetMeasurableId) { return measurableRatingDao.getSharedDecommsCount(measurableId, targetMeasurableId); } + + public Collection saveRatingItem(EntityReference entityRef, long measurableId, String ratingCode, String username) { + checkRatingIsAllowable(measurableCategoryId, entityRef, ratingCode); + return measurableRatingDao.; + } + + + // ---- HELPER ----- + + private void checkRatingIsAllowable(SaveMeasurableRatingCommand command) { + + long measurableCategory = measurableDao.getById(command.measurableId()).categoryId(); + EntityReference entityReference = command.entityReference(); + String ratingCode = Character.toString(command.rating()); + + checkRatingIsAllowable(measurableCategory, entityReference, ratingCode); + } + + private void checkRatingIsAllowable(long measurableCategory, EntityReference entityReference, String ratingCode) { + Boolean isRestricted = ratingSchemeService + .findRatingSchemeItemsForEntityAndCategory(entityReference, measurableCategory) + .stream() + .filter(r -> r.rating().equals(ratingCode)) + .map(RatingSchemeItem::isRestricted) + .findFirst() + .orElse(false); + + checkFalse(isRestricted, "New rating is restricted, rating not saved"); + } + } diff --git a/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/MeasurableRatingEndpoint.java b/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/MeasurableRatingEndpoint.java index b2c08ab59..000f19c64 100644 --- a/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/MeasurableRatingEndpoint.java +++ b/waltz-web/src/main/java/org/finos/waltz/web/endpoints/api/MeasurableRatingEndpoint.java @@ -59,7 +59,6 @@ public class MeasurableRatingEndpoint implements Endpoint { private final MeasurableRatingService measurableRatingService; private final MeasurableRatingPermissionChecker measurableRatingPermissionChecker; - private final MeasurableService measurableService; private final UserRoleService userRoleService; private final PermissionGroupService permissionGroupService; @@ -78,7 +77,6 @@ public MeasurableRatingEndpoint(MeasurableRatingService measurableRatingService, checkNotNull(measurableRatingPermissionChecker, "measurableRatingPermissionChecker cannot be null"); this.measurableRatingService = measurableRatingService; - this.measurableService = measurableService; this.permissionGroupService = permissionGroupService; this.measurableRatingPermissionChecker = measurableRatingPermissionChecker; this.userRoleService = userRoleService; @@ -97,6 +95,10 @@ public void register() { String statsByAppSelectorPath = mkPath(BASE_URL, "stats-by", "app-selector"); String statsForRelatedMeasurablePath = mkPath(BASE_URL, "related-stats", "measurable"); + String saveRatingItem = mkPath(BASE_URL, "entity", ":kind", ":id", "measurable", ":measurableId", "rating"); + String saveRatingDescription = mkPath(BASE_URL, "entity", ":kind", ":id", "measurable", ":measurableId", "description"); + String saveRatingIsPrimary = mkPath(BASE_URL, "entity", ":kind", ":id", "measurable", ":measurableId", "isPrimary"); + ListRoute findForEntityRoute = (request, response) -> measurableRatingService.findForEntity(getEntityReference(request)); @@ -128,6 +130,24 @@ public void register() { getForList(countByMeasurableCategoryPath, countByMeasurableCategoryRoute); postForList(statsForRelatedMeasurablePath, statsForRelatedMeasurableRoute); postForList(statsByAppSelectorPath, statsByAppSelectorRoute); + + postForList(saveRatingItem, this::saveRatingItemRoute); +// postForList(saveRatingDescription, this::saveRatingDescriptionRoute); +// postForList(saveRatingIsPrimary, this::saveRatingIsPrimaryRoute); + } + + private Collection saveRatingItemRoute(Request request, Response response) throws InsufficientPrivelegeException { + EntityReference entityRef = getEntityReference(request); + long measurableId = getLong(request, "measurableId"); + String ratingCode = request.body(); + + checkHasPermissionForThisOperation(entityRef, measurableId, asSet(Operation.UPDATE), getUsername(request)); + + return measurableRatingService.saveRatingItem( + entityRef, + measurableId, + ratingCode, + getUsername(request)); } private Collection removeCategoryRoute(Request request, Response z) { @@ -154,6 +174,7 @@ private Collection saveRoute(Request request, Response z) thro } + private Collection removeRoute(Request request, Response z) throws IOException, InsufficientPrivelegeException { long measurableId = getLong(request, "measurableId"); String username = getUsername(request);