From a0b61d5e1fdae89d17768dc68115b72598b90e9a Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Jun 2024 16:19:49 +0100 Subject: [PATCH 01/20] Database schema --- .../V106__create_volunteering_tables.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 server/src/main/resources/db/common/V106__create_volunteering_tables.sql diff --git a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql new file mode 100644 index 0000000000..2ea507c10f --- /dev/null +++ b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql @@ -0,0 +1,29 @@ +DROP TABLE IF EXISTS volunteering_relationship; +DROP TABLE IF EXISTS volunteering_event; +DROP TABLE IF EXISTS volunteering_organization; + +CREATE TABLE volunteering_organization +( + organization_id varchar PRIMARY KEY, + description varchar, + website varchar, + is_active boolean NOT NULL DEFAULT TRUE +); + +CREATE TABLE volunteering_relationship +( + relationship_id varchar PRIMARY KEY, + member_id varchar REFERENCES member_profile (id), + organization_id varchar REFERENCES volunteering_organization (organization_id), + start_date timestamp NOT NULL, + end_date timestamp +); + +CREATE TABLE volunteering_event +( + event_id varchar PRIMARY KEY, + relationship_id varchar REFERENCES volunteering_relationship (relationship_id), + event_date timestamp NOT NULL, + hours integer NOT NULL DEFAULT 0, + notes varchar +); From d679a683281b1b37c26b0ad470ab7404b6a7851c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Jun 2024 16:21:37 +0100 Subject: [PATCH 02/20] Description and website shouldn't be null --- .../resources/db/common/V106__create_volunteering_tables.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql index 2ea507c10f..fb426ab457 100644 --- a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql +++ b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql @@ -5,8 +5,8 @@ DROP TABLE IF EXISTS volunteering_organization; CREATE TABLE volunteering_organization ( organization_id varchar PRIMARY KEY, - description varchar, - website varchar, + description varchar NOT NULL, + website varchar NOT NULL, is_active boolean NOT NULL DEFAULT TRUE ); From e8565bb8a69aeef1b2c5f3f130aca9047de2837c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Jun 2024 16:53:50 +0100 Subject: [PATCH 03/20] Fix db, test data and JPA entities --- .../volunteering/VolunteeringEvent.java | 58 ++++++++++++++++++ .../VolunteeringOrganization.java | 50 +++++++++++++++ .../VolunteeringRelationship.java | 61 +++++++++++++++++++ .../V106__create_volunteering_tables.sql | 3 +- .../resources/db/dev/R__Load_testing_data.sql | 39 ++++++++++++ 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java new file mode 100644 index 0000000000..7cdd8a2add --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java @@ -0,0 +1,58 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.objectcomputing.checkins.converter.LocalDateConverter; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.UUID; + +@Setter +@Getter +@Entity +@Introspected +@Table(name = "volunteering_event") +public class VolunteeringEvent { + + @Id + @Column(name = "event_id") + @AutoPopulated + @TypeDef(type = DataType.STRING) + @Schema(description = "id of the volunteering event") + private UUID id; + + @Column(name = "relationship_id") + @NotNull + @Schema(description = "id of the Volunteering relationship") + private UUID relationshipId; + + @NotNull + @Column(name = "event_date") + @TypeDef(type = DataType.DATE, converter = LocalDateConverter.class) + @Schema(description = "when the volunteering event occurred") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate eventDate; + + @Column(name = "hours") + @Schema(description = "number of hours spent volunteering") + @TypeDef(type = DataType.INTEGER) + private int hours; + + @Nullable + @Column(name = "hours") + @TypeDef(type = DataType.STRING) + @Schema(description = "notes about the volunteering event") + private String notes; +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java new file mode 100644 index 0000000000..064caf4d6d --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java @@ -0,0 +1,50 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Setter +@Getter +@Entity +@Introspected +@Table(name = "volunteering_organization") +public class VolunteeringOrganization { + + @Id + @Column(name = "organization_id") + @AutoPopulated + @TypeDef(type = DataType.STRING) + @Schema(description = "id of the volunteering organization") + private UUID id; + + @Column(name = "name") + @NotBlank + @Schema(description = "name of the volunteering organization") + private String name; + + @Column(name = "description") + @NotBlank + @Schema(description = "description of the volunteering organization") + private String description; + + @Column(name = "website") + @NotBlank + @Schema(description = "website for the volunteering organization") + private String website; + + @Column(name = "is_active") + @Schema(description = "whether the Volunteering Organization is active") + private boolean active = true; +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java new file mode 100644 index 0000000000..831ba3db43 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java @@ -0,0 +1,61 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.objectcomputing.checkins.converter.LocalDateConverter; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.UUID; + +@Setter +@Getter +@Entity +@Introspected +@Table(name = "volunteering_relationship") +public class VolunteeringRelationship { + + @Id + @Column(name = "relationship_id") + @AutoPopulated + @TypeDef(type = DataType.STRING) + @Schema(description = "id of the volunteering relationship") + private UUID id; + + @NotNull + @Column(name = "member_id") + @TypeDef(type = DataType.STRING) + @Schema(description = "id of the member with the relationship") + private UUID memberId; + + @NotNull + @Column(name = "organization_id") + @TypeDef(type = DataType.STRING) + @Schema(description = "id of the organization with the relationship") + private UUID organizationId; + + @NotNull + @Column(name = "start_date") + @TypeDef(type = DataType.DATE, converter = LocalDateConverter.class) + @Schema(description = "when the relationship started") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @Nullable + @Column(name = "end_date") + @TypeDef(type = DataType.DATE, converter = LocalDateConverter.class) + @Schema(description = "(optionally) when the relationship ended") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate endDate; +} \ No newline at end of file diff --git a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql index fb426ab457..5c5aa6777d 100644 --- a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql +++ b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql @@ -1,10 +1,11 @@ -DROP TABLE IF EXISTS volunteering_relationship; DROP TABLE IF EXISTS volunteering_event; +DROP TABLE IF EXISTS volunteering_relationship; DROP TABLE IF EXISTS volunteering_organization; CREATE TABLE volunteering_organization ( organization_id varchar PRIMARY KEY, + name varchar NOT NULL, description varchar NOT NULL, website varchar NOT NULL, is_active boolean NOT NULL DEFAULT TRUE diff --git a/server/src/main/resources/db/dev/R__Load_testing_data.sql b/server/src/main/resources/db/dev/R__Load_testing_data.sql index 78f5f22998..a167230681 100644 --- a/server/src/main/resources/db/dev/R__Load_testing_data.sql +++ b/server/src/main/resources/db/dev/R__Load_testing_data.sql @@ -1520,4 +1520,43 @@ INSERT INTO skillcategory_skills -- Tools CSS values ('0778a8e7-21d8-4ca3-a0dc-cad676aac417', '6b56f0aa-09aa-4b09-bb81-03481af7e49f'); +-- Volunteering + +INSERT INTO volunteering_organization + (organization_id, name, description, website) +VALUES ('c3381858-9745-4084-928e-ddbc44275f92', 'Lift for Life', 'Educate, Empower, Uplift', + 'https://www.liftforlifeacademy.org/'); + +INSERT INTO volunteering_organization + (organization_id, name, description, website) +VALUES ('fbb31840-a247-4524-ae35-1c84263849bf', 'St. Louis Area Foodbank', + 'Works with over 600 partners in 26 counties across the bi-state area to provide options for those in need of food', + 'https://stlfoodbank.org/find-food/'); + +INSERT INTO volunteering_relationship + (relationship_id, member_id, organization_id, start_date, end_date) +VALUES -- Michael Kimberlin to Lift for Life + ('b2ffbfb0-efd2-4305-b741-b95db5ee36a8', '6207b3fd-042d-49aa-9e28-dcc04f537c2d', + 'c3381858-9745-4084-928e-ddbc44275f92', '2021-01-01', '2022-01-01'); + +INSERT INTO volunteering_relationship + (relationship_id, member_id, organization_id, start_date) +VALUES -- Mark Volkmann to St. Louis Area Foodbank + ('7c945589-48c4-4474-8298-74b343de34ec', '2c1b77e2-e2fc-46d1-92f2-beabbd28ee3d', + 'fbb31840-a247-4524-ae35-1c84263849bf', '2024-04-16'); + +INSERT INTO volunteering_event + (event_id, relationship_id, event_date, hours, notes) +VALUES + ('12a45a85-7c67-4f9f-9b1c-672acb38411a', 'b2ffbfb0-efd2-4305-b741-b95db5ee36a8', '2024-02-14', 4, 'first event'); + +INSERT INTO volunteering_event + (event_id, relationship_id, event_date, hours, notes) +VALUES + ('8969ad87-a299-4ae8-b10d-d7e3b6072a09', 'b2ffbfb0-efd2-4305-b741-b95db5ee36a8', '2024-05-01', 8, 'second event'); + +INSERT INTO volunteering_event + (event_id, relationship_id, event_date, hours, notes) +VALUES + ('2afba083-8d42-429f-a90f-8992d1685bd0', 'b2ffbfb0-efd2-4305-b741-b95db5ee36a8', '2024-05-02', 4, 'third event'); From 00d56cc31b6fe3b8bd834836e04c6e10977488f2 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Jun 2024 17:05:34 +0100 Subject: [PATCH 04/20] Skeleton repositories and service interface --- .../VolunteeringEventRepository.java | 11 ++++++++ .../VolunteeringOrganizationRepository.java | 11 ++++++++ .../VolunteeringRelationshipRepository.java | 11 ++++++++ .../volunteering/VolunteeringService.java | 27 +++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java new file mode 100644 index 0000000000..f1a46cc578 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java @@ -0,0 +1,11 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; + +import java.util.UUID; + +@JdbcRepository(dialect = Dialect.POSTGRES) +public interface VolunteeringEventRepository extends CrudRepository { +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java new file mode 100644 index 0000000000..d40d92a121 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java @@ -0,0 +1,11 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; + +import java.util.UUID; + +@JdbcRepository(dialect = Dialect.POSTGRES) +public interface VolunteeringOrganizationRepository extends CrudRepository { +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java new file mode 100644 index 0000000000..9cb13c1593 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java @@ -0,0 +1,11 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.repository.CrudRepository; + +import java.util.UUID; + +@JdbcRepository(dialect = Dialect.POSTGRES) +public interface VolunteeringRelationshipRepository extends CrudRepository { +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java new file mode 100644 index 0000000000..065df56099 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java @@ -0,0 +1,27 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.core.annotation.Nullable; + +import java.util.List; +import java.util.UUID; + +public interface VolunteeringService { + + List listOrganizations(boolean showInactive); + + List listRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, boolean showInactive); + + List listEvents(@Nullable UUID relationshipId, @Nullable UUID memberId, @Nullable UUID organizationId, boolean showInactive); + + VolunteeringOrganization create(VolunteeringOrganization organization); + + VolunteeringRelationship create(VolunteeringRelationship organization); + + VolunteeringEvent create(VolunteeringEvent organization); + + VolunteeringOrganization update(VolunteeringOrganization organization); + + VolunteeringRelationship update(VolunteeringRelationship organization); + + VolunteeringEvent update(VolunteeringEvent organization); +} From 1dc349289573d78517ec049704925f45ed2f3c4c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Jun 2024 18:23:26 +0100 Subject: [PATCH 05/20] Add is_active to volunteering relationship --- .../services/volunteering/VolunteeringRelationship.java | 4 ++++ .../resources/db/common/V106__create_volunteering_tables.sql | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java index 831ba3db43..20acfcdcd8 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java @@ -58,4 +58,8 @@ public class VolunteeringRelationship { @Schema(description = "(optionally) when the relationship ended") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private LocalDate endDate; + + @Column(name = "is_active") + @Schema(description = "whether the Volunteering Relationship is active") + private boolean active = true; } \ No newline at end of file diff --git a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql index 5c5aa6777d..57db340951 100644 --- a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql +++ b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql @@ -17,7 +17,8 @@ CREATE TABLE volunteering_relationship member_id varchar REFERENCES member_profile (id), organization_id varchar REFERENCES volunteering_organization (organization_id), start_date timestamp NOT NULL, - end_date timestamp + end_date timestamp, + is_active boolean NOT NULL DEFAULT TRUE ); CREATE TABLE volunteering_event From ca382dad0f9ff59b7b58e5de5cbc573e5292d552 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 7 Jun 2024 18:50:59 +0100 Subject: [PATCH 06/20] Add controller and findAll methods through to repo via service --- .../volunteering/VolunteeringController.java | 59 +++++++++++++ .../VolunteeringEventRepository.java | 34 ++++++++ .../VolunteeringOrganizationRepository.java | 9 ++ .../VolunteeringRelationshipRepository.java | 42 ++++++++++ .../volunteering/VolunteeringService.java | 6 +- .../volunteering/VolunteeringServiceImpl.java | 84 +++++++++++++++++++ 6 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java new file mode 100644 index 0000000000..55d01cdb83 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java @@ -0,0 +1,59 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; + +import java.util.List; +import java.util.UUID; + +@Controller("/services/volunteer") +class VolunteeringController { + + private final VolunteeringService volunteeringService; + + public VolunteeringController(VolunteeringService volunteeringService) { + this.volunteeringService = volunteeringService; + } + + /** + * List all volunteering organizations + * + * @param includeDeactivated whether to include deactivated organizations + * @return list of {@link VolunteeringOrganization} + */ + @Get("/organization/{?includeDeactivated}") + List findAll(@Nullable Boolean includeDeactivated) { + return volunteeringService.listOrganizations(Boolean.TRUE.equals(includeDeactivated)); + } + + /** + * List all volunteering relationships + * If memberId is provided, restrict to relationships for that member. + * If organizationId is provided, restrict to relationships for that organization. + * If includeInactive is true, include inactive relationships and organizations in the results. + * + * @param memberId the id of the member + * @param organizationId the id of the organization + * @param includeDeactivated whether to include deactivated relationships or organizations + * @return list of {@link VolunteeringRelationship} + */ + @Get("/relationship/{?memberId,organizationId,includeDeactivated}") + List findRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated) { + return volunteeringService.listRelationships(memberId, organizationId, Boolean.TRUE.equals(includeDeactivated)); + } + + /** + * List all volunteering events. + * If relationshipId is provided, restrict to events for that relationship. + * If includeInactive is true, include inactive organizations and relationships in the results. + * + * @param relationshipId the id of the relationship + * @param includeDeactivated whether to include deactivated relationships or organizations + * @return list of {@link VolunteeringEvent} + */ + @Get("/event/{?relationshipId,includeDeactivated}") + List findEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, @Nullable Boolean includeDeactivated) { + return volunteeringService.listEvents(memberId, relationshipId, Boolean.TRUE.equals(includeDeactivated)); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java index f1a46cc578..e858d28fd0 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java @@ -1,11 +1,45 @@ package com.objectcomputing.checkins.services.volunteering; +import io.micronaut.data.annotation.Query; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; +import java.util.List; import java.util.UUID; @JdbcRepository(dialect = Dialect.POSTGRES) public interface VolunteeringEventRepository extends CrudRepository { + + @Query(""" + SELECT event.* + FROM volunteering_event AS event + LEFT JOIN volunteering_relationship AS rel USING(relationship_id) + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE rel.member_id = :memberId + AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY event.event_date, org.name""") + List findByMemberId(UUID memberId, boolean includeDeactivated); + + @Query(""" + SELECT event.* + FROM volunteering_event AS event + LEFT JOIN volunteering_relationship AS rel USING(relationship_id) + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE rel.relationship_id = :relationshipId + AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY event.event_date, org.name""") + List findByRelationshipId(UUID relationshipId, boolean includeDeactivated); + + @Query(""" + SELECT event.* + FROM volunteering_event AS event + LEFT JOIN volunteering_relationship AS rel USING(relationship_id) + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY event.event_date, org.name""") + List findAll(boolean includeDeactivated); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java index d40d92a121..acb8947b6e 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java @@ -1,11 +1,20 @@ package com.objectcomputing.checkins.services.volunteering; +import io.micronaut.data.annotation.Query; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; +import java.util.List; import java.util.UUID; @JdbcRepository(dialect = Dialect.POSTGRES) public interface VolunteeringOrganizationRepository extends CrudRepository { + + @Query(""" + SELECT org.* + FROM volunteering_organization AS org + WHERE org.is_active = TRUE OR :includeDeactivated = TRUE + ORDER BY org.name""") + List findAll(boolean includeDeactivated); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java index 9cb13c1593..a7d25d5489 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipRepository.java @@ -1,11 +1,53 @@ package com.objectcomputing.checkins.services.volunteering; +import io.micronaut.data.annotation.Query; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; +import java.util.List; import java.util.UUID; @JdbcRepository(dialect = Dialect.POSTGRES) public interface VolunteeringRelationshipRepository extends CrudRepository { + + @Query(""" + SELECT rel.* + FROM volunteering_relationship AS rel + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE rel.organization_id = :organizationId + AND rel.member_id = :memberId + AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY rel.start_date, org.name""") + List findByMemberIdAndOrganizationId(UUID memberId, UUID organizationId, boolean includeDeactivated); + + @Query(""" + SELECT rel.* + FROM volunteering_relationship AS rel + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE rel.member_id = :memberId + AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY rel.start_date, org.name""") + List findByMemberId(UUID memberId, boolean includeDeactivated); + + @Query(""" + SELECT rel.* + FROM volunteering_relationship AS rel + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE rel.organization_id = :organizationId + AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY rel.start_date, org.name""") + List findByOrganizationId(UUID organizationId, boolean includeDeactivated); + + @Query(""" + SELECT rel.* + FROM volunteering_relationship AS rel + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY rel.start_date, org.name""") + List findAll(boolean includeDeactivated); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java index 065df56099..80bce4495c 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java @@ -7,11 +7,11 @@ public interface VolunteeringService { - List listOrganizations(boolean showInactive); + List listOrganizations(boolean includeDeactivated); - List listRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, boolean showInactive); + List listRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, boolean includeDeactivated); - List listEvents(@Nullable UUID relationshipId, @Nullable UUID memberId, @Nullable UUID organizationId, boolean showInactive); + List listEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, boolean includeDeactivated); VolunteeringOrganization create(VolunteeringOrganization organization); diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java new file mode 100644 index 0000000000..fbba5eabc6 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -0,0 +1,84 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.core.annotation.Nullable; +import jakarta.inject.Singleton; + +import java.util.List; +import java.util.UUID; + +@Singleton +class VolunteeringServiceImpl implements VolunteeringService { + + private final VolunteeringOrganizationRepository organizationRepo; + private final VolunteeringRelationshipRepository relationshipRepo; + private final VolunteeringEventRepository eventRepo; + + VolunteeringServiceImpl( + VolunteeringOrganizationRepository organizationRepo, + VolunteeringRelationshipRepository relationshipRepo, + VolunteeringEventRepository eventRepo + ) { + this.organizationRepo = organizationRepo; + this.relationshipRepo = relationshipRepo; + this.eventRepo = eventRepo; + } + + @Override + public List listOrganizations(boolean includeDeactivated) { + return organizationRepo.findAll(includeDeactivated); + } + + @Override + public List listRelationships(UUID memberId, UUID organizationId, boolean includeDeactivated) { + if (memberId != null && organizationId != null) { + return relationshipRepo.findByMemberIdAndOrganizationId(memberId, organizationId, includeDeactivated); + } else if (memberId != null) { + return relationshipRepo.findByMemberId(memberId, includeDeactivated); + } else if (organizationId != null) { + return relationshipRepo.findByOrganizationId(organizationId, includeDeactivated); + } else { + return relationshipRepo.findAll(includeDeactivated); + } + } + + @Override + public List listEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, boolean includeDeactivated) { + if (memberId != null) { + return eventRepo.findByMemberId(memberId, includeDeactivated); + } else if (relationshipId != null) { + return eventRepo.findByRelationshipId(relationshipId, includeDeactivated); + } else { + return eventRepo.findAll(includeDeactivated); + } + } + + @Override + public VolunteeringOrganization create(VolunteeringOrganization organization) { + return null; + } + + @Override + public VolunteeringRelationship create(VolunteeringRelationship organization) { + return null; + } + + @Override + public VolunteeringEvent create(VolunteeringEvent organization) { + return null; + } + + @Override + public VolunteeringOrganization update(VolunteeringOrganization organization) { + return null; + } + + @Override + public VolunteeringRelationship update(VolunteeringRelationship organization) { + return null; + } + + @Override + public VolunteeringEvent update(VolunteeringEvent organization) { + return null; + } +} From 50a042df314e27081f2d834225e7c65525f88802 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 10 Jun 2024 14:49:37 +0100 Subject: [PATCH 07/20] Add permissions and test organizations --- .../services/permissions/Permission.java | 5 +- .../volunteering/VolunteeringController.java | 89 +++++++++++- .../VolunteeringOrganization.java | 12 ++ .../VolunteeringOrganizationDTO.java | 34 +++++ .../VolunteeringOrganizationRepository.java | 14 +- .../VolunteeringRelationship.java | 8 ++ .../VolunteeringRelationshipDTO.java | 47 ++++++ .../volunteering/VolunteeringService.java | 8 +- .../volunteering/VolunteeringServiceImpl.java | 28 +++- .../V106__create_volunteering_tables.sql | 2 +- .../resources/db/dev/R__Load_testing_data.sql | 14 ++ .../services/TestContainersSuite.java | 3 + .../services/fixture/PermissionFixture.java | 5 +- .../services/fixture/RepositoryFixture.java | 15 ++ .../services/fixture/VolunteeringFixture.java | 22 +++ .../VolunteeringControllerTest.java | 136 ++++++++++++++++++ 16 files changed, 427 insertions(+), 15 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java index 063080394b..7fff2524e9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java @@ -48,7 +48,10 @@ public enum Permission { CAN_DELETE_REVIEW_PERIOD("Delete review periods", "Review Periods"), CAN_ADMINISTER_SETTINGS("Add or edit settings", "Settings"), CAN_VIEW_SETTINGS("View settings", "Settings"), - CAN_VIEW_ALL_PULSE_RESPONSES("View pulse responses", "Reporting"); + CAN_VIEW_ALL_PULSE_RESPONSES("View pulse responses", "Reporting"), + CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS("Update volunteering organizations", "Volunteering"), + CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS("Update volunteering relationships", "Volunteering"), + CAN_ADMINISTER_VOLUNTEERING_HOURS("Update volunteering hours", "Volunteering"); private final String description; private final String category; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java index 55d01cdb83..a81bcb4019 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java @@ -1,12 +1,30 @@ package com.objectcomputing.checkins.services.volunteering; +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.permissions.RequiredPermission; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.annotation.Status; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; +import static io.micronaut.http.HttpStatus.CREATED; + +@ExecuteOn(TaskExecutors.BLOCKING) +@Secured(SecurityRule.IS_AUTHENTICATED) +@Tag(name = "volunteering") @Controller("/services/volunteer") class VolunteeringController { @@ -22,11 +40,80 @@ public VolunteeringController(VolunteeringService volunteeringService) { * @param includeDeactivated whether to include deactivated organizations * @return list of {@link VolunteeringOrganization} */ - @Get("/organization/{?includeDeactivated}") + @Get("/organization{?includeDeactivated}") List findAll(@Nullable Boolean includeDeactivated) { return volunteeringService.listOrganizations(Boolean.TRUE.equals(includeDeactivated)); } + /** + * Create a new volunteering organization + * + * @param organization the organization to create + * @return the created {@link VolunteeringOrganization} + */ + @Post("/organization") + @Status(CREATED) + @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) + VolunteeringOrganization create(@Valid @Body VolunteeringOrganizationDTO organization) { + return volunteeringService.create(new VolunteeringOrganization( + organization.getName(), + organization.getDescription(), + organization.getWebsite() + )); + } + + /** + * Update an existing volunteering organization + * + * @param organization the organization to update + * @return the updated {@link VolunteeringOrganization} + */ + @Put("/organization/{id}") + @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) + VolunteeringOrganization update(@NotNull UUID id, @Valid @Body VolunteeringOrganizationDTO organization) { + return volunteeringService.update(new VolunteeringOrganization( + id, + organization.getName(), + organization.getDescription(), + organization.getWebsite(), + Boolean.TRUE.equals(organization.getActive()) + )); + } + + /** + * Create a new volunteering relationship + * + * @param relationship the relationship to create + * @return the created {@link VolunteeringRelationship} + */ + @Post("/relationship") + VolunteeringRelationship create(@Valid @Body VolunteeringRelationshipDTO relationship) { + return volunteeringService.create(new VolunteeringRelationship( + relationship.getMemberId(), + relationship.getOrganizationId(), + relationship.getStartDate(), + relationship.getEndDate() + )); + } + + /** + * Update an existing volunteering relationship + * + * @param relationship the relationship to update + * @return the updated {@link VolunteeringRelationship} + */ + @Put("/relationship/{id}") + VolunteeringRelationship update(@NotNull UUID id, @Valid @Body VolunteeringRelationshipDTO relationship) { + return volunteeringService.update(new VolunteeringRelationship( + id, + relationship.getMemberId(), + relationship.getOrganizationId(), + relationship.getStartDate(), + relationship.getEndDate(), + Boolean.TRUE.equals(relationship.getActive()) + )); + } + /** * List all volunteering relationships * If memberId is provided, restrict to relationships for that member. diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java index 064caf4d6d..994f82a672 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganization.java @@ -10,7 +10,9 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.util.UUID; @@ -19,6 +21,8 @@ @Getter @Entity @Introspected +@AllArgsConstructor +@NoArgsConstructor @Table(name = "volunteering_organization") public class VolunteeringOrganization { @@ -47,4 +51,12 @@ public class VolunteeringOrganization { @Column(name = "is_active") @Schema(description = "whether the Volunteering Organization is active") private boolean active = true; + + public VolunteeringOrganization(String name, String description, String website) { + this(null, name, description, website, true); + } + + public VolunteeringOrganization(String name, String description, String website, boolean active) { + this(null, name, description, website, active); + } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java new file mode 100644 index 0000000000..14ed90e07c --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java @@ -0,0 +1,34 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.core.annotation.Introspected; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +@Introspected +public class VolunteeringOrganizationDTO { + + @NotBlank + @Schema(description = "name of the volunteering organization") + private String name; + + @NotBlank + @Schema(description = "description of the volunteering organization") + private String description; + + @NotBlank + @Schema(description = "website for the volunteering organization") + private String website; + + @Schema(description = "whether the Volunteering Organization is active") + private Boolean active; + + public VolunteeringOrganizationDTO(String name, String description, String website) { + this(name, description, website, true); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java index acb8947b6e..ac2bb56441 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java @@ -1,20 +1,24 @@ package com.objectcomputing.checkins.services.volunteering; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.data.annotation.Query; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; import java.util.List; +import java.util.Optional; import java.util.UUID; @JdbcRepository(dialect = Dialect.POSTGRES) public interface VolunteeringOrganizationRepository extends CrudRepository { + Optional getByName(String name); + @Query(""" - SELECT org.* - FROM volunteering_organization AS org - WHERE org.is_active = TRUE OR :includeDeactivated = TRUE - ORDER BY org.name""") + SELECT org.* + FROM volunteering_organization AS org + WHERE org.is_active = TRUE OR :includeDeactivated = TRUE + ORDER BY org.name""") List findAll(boolean includeDeactivated); -} +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java index 20acfcdcd8..5987554f3d 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java @@ -13,7 +13,9 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDate; @@ -23,6 +25,8 @@ @Getter @Entity @Introspected +@AllArgsConstructor +@NoArgsConstructor @Table(name = "volunteering_relationship") public class VolunteeringRelationship { @@ -62,4 +66,8 @@ public class VolunteeringRelationship { @Column(name = "is_active") @Schema(description = "whether the Volunteering Relationship is active") private boolean active = true; + + VolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, LocalDate endDate) { + this(null, memberId, organizationId, startDate, endDate, true); + } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java new file mode 100644 index 0000000000..2f85a50ae1 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java @@ -0,0 +1,47 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.objectcomputing.checkins.converter.LocalDateConverter; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.UUID; + +@Setter +@Getter +@Introspected +public class VolunteeringRelationshipDTO { + + @NotNull + @Schema(description = "id of the member with the relationship") + private UUID memberId; + + @NotNull + @Schema(description = "id of the organization with the relationship") + private UUID organizationId; + + @NotNull + @Schema(description = "when the relationship started") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @Nullable + @Schema(description = "(optionally) when the relationship ended") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate endDate; + + @Schema(description = "whether the Volunteering Relationship is active") + private Boolean active = true; +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java index 80bce4495c..ba15d263ba 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java @@ -15,13 +15,13 @@ public interface VolunteeringService { VolunteeringOrganization create(VolunteeringOrganization organization); - VolunteeringRelationship create(VolunteeringRelationship organization); + VolunteeringRelationship create(VolunteeringRelationship relationship); - VolunteeringEvent create(VolunteeringEvent organization); + VolunteeringEvent create(VolunteeringEvent event); VolunteeringOrganization update(VolunteeringOrganization organization); - VolunteeringRelationship update(VolunteeringRelationship organization); + VolunteeringRelationship update(VolunteeringRelationship relationship); - VolunteeringEvent update(VolunteeringEvent organization); + VolunteeringEvent update(VolunteeringEvent event); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index fbba5eabc6..cfa95046dc 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -1,7 +1,10 @@ package com.objectcomputing.checkins.services.volunteering; +import com.objectcomputing.checkins.exceptions.BadArgException; import io.micronaut.core.annotation.Nullable; import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.UUID; @@ -9,6 +12,9 @@ @Singleton class VolunteeringServiceImpl implements VolunteeringService { + private static final Logger LOG = LoggerFactory.getLogger(VolunteeringServiceImpl.class); + private static final String ORG_NAME_ALREADY_EXISTS_MESSAGE = "Volunteering Organization with name %s already exists"; + private final VolunteeringOrganizationRepository organizationRepo; private final VolunteeringRelationshipRepository relationshipRepo; private final VolunteeringEventRepository eventRepo; @@ -54,7 +60,14 @@ public List listEvents(@Nullable UUID memberId, @Nullable UUI @Override public VolunteeringOrganization create(VolunteeringOrganization organization) { - return null; + if (organization.getId() != null) { + return update(organization); + } + // Fail if a certification with the same name already exists + validate(organizationRepo.getByName(organization.getName()).isPresent(), + ORG_NAME_ALREADY_EXISTS_MESSAGE, + organization.getName()); + return organizationRepo.save(organization); } @Override @@ -69,7 +82,12 @@ public VolunteeringEvent create(VolunteeringEvent organization) { @Override public VolunteeringOrganization update(VolunteeringOrganization organization) { - return null; + // Fail if a certification with the same name already exists (but it's not this one) + validate(organizationRepo.getByName(organization.getName()) + .map(c -> !c.getId().equals(organization.getId())).orElse(false), + ORG_NAME_ALREADY_EXISTS_MESSAGE, + organization.getName()); + return organizationRepo.update(organization); } @Override @@ -81,4 +99,10 @@ public VolunteeringRelationship update(VolunteeringRelationship organization) { public VolunteeringEvent update(VolunteeringEvent organization) { return null; } + + private void validate(boolean isError, String message, Object... args) { + if (isError) { + throw new BadArgException(String.format(message, args)); + } + } } diff --git a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql index 57db340951..b2c06c4b5a 100644 --- a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql +++ b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql @@ -5,7 +5,7 @@ DROP TABLE IF EXISTS volunteering_organization; CREATE TABLE volunteering_organization ( organization_id varchar PRIMARY KEY, - name varchar NOT NULL, + name varchar NOT NULL UNIQUE, description varchar NOT NULL, website varchar NOT NULL, is_active boolean NOT NULL DEFAULT TRUE diff --git a/server/src/main/resources/db/dev/R__Load_testing_data.sql b/server/src/main/resources/db/dev/R__Load_testing_data.sql index a167230681..353713f690 100644 --- a/server/src/main/resources/db/dev/R__Load_testing_data.sql +++ b/server/src/main/resources/db/dev/R__Load_testing_data.sql @@ -980,6 +980,20 @@ insert into role_permissions values ('d03f5f0b-e29c-4cf4-9ea4-6baa09405c56', 'CAN_VIEW_REVIEW_PERIOD'); +insert into role_permissions +(roleid, permission) +values + ('d03f5f0b-e29c-4cf4-9ea4-6baa09405c56', 'CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS'); + +insert into role_permissions +(roleid, permission) +values + ('d03f5f0b-e29c-4cf4-9ea4-6baa09405c56', 'CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS'); + +insert into role_permissions +(roleid, permission) +values + ('d03f5f0b-e29c-4cf4-9ea4-6baa09405c56', 'CAN_ADMINISTER_VOLUNTEERING_HOURS'); -- Member permissions insert into role_permissions diff --git a/server/src/test/java/com/objectcomputing/checkins/services/TestContainersSuite.java b/server/src/test/java/com/objectcomputing/checkins/services/TestContainersSuite.java index 5ae2d035b4..8203ee8b72 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/TestContainersSuite.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/TestContainersSuite.java @@ -30,6 +30,9 @@ public TestContainersSuite() {} private void deleteAllEntities() { // Note order can matter here. + getVolunteeringEventRepository().deleteAll(); + getVolunteeringRelationshipRepository().deleteAll(); + getVolunteeringOrganizationRepository().deleteAll(); getEntityTagRepository().deleteAll(); getTagRepository().deleteAll(); getPulseResponseRepository().deleteAll(); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java index b96425ad7b..a8a6d9682b 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java @@ -85,7 +85,10 @@ public interface PermissionFixture extends RolePermissionFixture { Permission.CAN_LAUNCH_REVIEW_PERIOD, Permission.CAN_CLOSE_REVIEW_PERIOD, Permission.CAN_DELETE_REVIEW_PERIOD, - Permission.CAN_VIEW_ALL_PULSE_RESPONSES + Permission.CAN_VIEW_ALL_PULSE_RESPONSES, + Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS, + Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS, + Permission.CAN_ADMINISTER_VOLUNTEERING_HOURS ); default void setPermissionsForAdmin(UUID roleID) { diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java index cad610022d..dfe8371465 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/RepositoryFixture.java @@ -35,6 +35,9 @@ import com.objectcomputing.checkins.services.team.TeamRepository; import com.objectcomputing.checkins.services.team.member.MemberHistoryRepository; import com.objectcomputing.checkins.services.team.member.TeamMemberRepository; +import com.objectcomputing.checkins.services.volunteering.VolunteeringEventRepository; +import com.objectcomputing.checkins.services.volunteering.VolunteeringOrganizationRepository; +import com.objectcomputing.checkins.services.volunteering.VolunteeringRelationshipRepository; import io.micronaut.runtime.server.EmbeddedServer; import com.objectcomputing.checkins.services.survey.SurveyRepository; import com.objectcomputing.checkins.services.employee_hours.EmployeeHoursRepository; @@ -193,4 +196,16 @@ default SkillCategorySkillRepository getSkillCategorySkillRepository() { default MemberProfileReportRepository getMemberProfileReportRepository() { return getEmbeddedServer().getApplicationContext().getBean(MemberProfileReportRepository.class); } + + default VolunteeringOrganizationRepository getVolunteeringOrganizationRepository() { + return getEmbeddedServer().getApplicationContext().getBean(VolunteeringOrganizationRepository.class); + } + + default VolunteeringRelationshipRepository getVolunteeringRelationshipRepository() { + return getEmbeddedServer().getApplicationContext().getBean(VolunteeringRelationshipRepository.class); + } + + default VolunteeringEventRepository getVolunteeringEventRepository() { + return getEmbeddedServer().getApplicationContext().getBean(VolunteeringEventRepository.class); + } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java new file mode 100644 index 0000000000..1691ebff47 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java @@ -0,0 +1,22 @@ +package com.objectcomputing.checkins.services.fixture; + +import com.objectcomputing.checkins.services.volunteering.VolunteeringOrganization; + +public interface VolunteeringFixture extends RepositoryFixture { + + default VolunteeringOrganization createDefaultVolunteeringOrganization() { + return createVolunteeringOrganization( + "Test Organization", + "Test Description", + "www.test.com" + ); + } + + default VolunteeringOrganization createVolunteeringOrganization(String name, String description, String website) { + return createVolunteeringOrganization(name, description, website, true); + } + + default VolunteeringOrganization createVolunteeringOrganization(String name, String description, String website, boolean active) { + return getVolunteeringOrganizationRepository().save(new VolunteeringOrganization(name, description, website, active)); + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java new file mode 100644 index 0000000000..eab70f9831 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java @@ -0,0 +1,136 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +import com.objectcomputing.checkins.services.fixture.VolunteeringFixture; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import io.micronaut.core.type.Argument; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; +import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VolunteeringControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, VolunteeringFixture { + + @Inject + @Client("/services/volunteer") + HttpClient httpClient; + + BlockingHttpClient client; + + @BeforeEach + void makeRoles() { + client = httpClient.toBlocking(); + createAndAssignRoles(); + } + + @Test + void testCreateOrganization() { + MemberProfile memberProfile = createADefaultMemberProfile(); + List list = client.retrieve(HttpRequest.GET("/organization").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + assertEquals(0, list.size()); + + VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); + HttpResponse response = client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE), VolunteeringOrganization.class); + assertEquals(HttpStatus.CREATED, response.getStatus()); + assertNotNull(response.body().getId()); + assertEquals("name", response.body().getName()); + assertEquals("description", response.body().getDescription()); + assertEquals("website", response.body().getWebsite()); + + // List works as member without the profile + list = client.retrieve(HttpRequest.GET("/organization").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + assertEquals(1, list.size()); + assertEquals("name", list.getFirst().getName()); + assertEquals("description", list.getFirst().getDescription()); + assertEquals("website", list.getFirst().getWebsite()); + assertTrue(list.getFirst().isActive(), "Organization should be active by default"); + } + + @Test + void organizationsCanBeInactive() { + MemberProfile memberProfile = createADefaultMemberProfile(); + createVolunteeringOrganization("alpha", "alpha desc", "https://alpha.com"); + createVolunteeringOrganization("gamma", "gamma desc", "https://gamma.com"); + createVolunteeringOrganization("epsilon", "epsilon desc", "https://epsilon.com"); + createVolunteeringOrganization("beta", "beta desc", "https://beta.com", false); + + // List by default hides inactive (and they're ordered by name) + List list = client.retrieve(HttpRequest.GET("/organization").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + assertEquals(3, list.size()); + assertEquals(List.of("alpha", "epsilon", "gamma"), list.stream().map(VolunteeringOrganization::getName).toList()); + + // List with includeDeactivated shows all (and they're ordered by name) + list = client.retrieve(HttpRequest.GET("/organization?includeDeactivated=true").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + assertEquals(4, list.size()); + assertEquals(List.of("alpha", "beta", "epsilon", "gamma"), list.stream().map(VolunteeringOrganization::getName).toList()); + } + + @Test + void testCreateOrganizationWithoutRole() { + VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); + + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(MEMBER_ROLE, MEMBER_ROLE))); + assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); + } + + @Test + void testCreateOrganizationWithoutDuplicateName() { + MemberProfile memberProfile = createADefaultMemberProfile(); + createVolunteeringOrganization("name", "description", "website"); + VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); + + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Volunteering Organization with name name already exists", e.getMessage()); + } + + @Test + void testCreateOrganizationWithoutName() { + MemberProfile memberProfile = createADefaultMemberProfile(); + VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO(null, "description", "website"); + + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + String body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("organization.name: must not be blank"), body + " should contain 'organization.name: must not be blank'"); + } + + @Test + void testCreateOrganizationWithoutDescription() { + MemberProfile memberProfile = createADefaultMemberProfile(); + VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", null, "website"); + + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + String body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("organization.description: must not be blank"), body + " should contain 'organization.description: must not be blank'"); + } + + @Test + void testCreateOrganizationWithoutWebsite() { + MemberProfile memberProfile = createADefaultMemberProfile(); + VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", null); + + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + String body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("organization.website: must not be blank"), body + " should contain 'organization.website: must not be blank'"); + } +} From bfaf9624500293406498883e0f2ba33f9645ab00 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 10 Jun 2024 16:17:56 +0100 Subject: [PATCH 08/20] Use a client for Volunteering organization --- .../VolunteeringOrganizationClient.java | 34 ++++++++++ ...lunteeringOrganizationControllerTest.java} | 63 ++++++++++++------- 2 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java rename server/src/test/java/com/objectcomputing/checkins/services/volunteering/{VolunteeringControllerTest.java => VolunteeringOrganizationControllerTest.java} (67%) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java new file mode 100644 index 0000000000..a0a5c2e51d --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java @@ -0,0 +1,34 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.client.annotation.Client; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +@Client("/services/volunteer/organization") +@Requires(property = VolunteeringOrganizationClient.ENABLED, value = "true") +public interface VolunteeringOrganizationClient { + + String ENABLED = "enable.volunteering.organization.client"; + + @Get("/") + List getAllOrganizations(@Header String authorization); + + @Get("/{?includeDeactivated}") + List getAllOrganizations(@Header String authorization, @Nullable Boolean includeDeactivated); + + @Post + HttpResponse createOrganization(@Header String authorization, @Body VolunteeringOrganizationDTO organization); + + @Put("/{id}") + VolunteeringOrganization updateOrganization(@Header String authorization, @NotNull UUID id, @Body VolunteeringOrganizationDTO organization); +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java similarity index 67% rename from server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java rename to server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java index eab70f9831..bfa6cb20d8 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java @@ -5,18 +5,16 @@ import com.objectcomputing.checkins.services.fixture.RoleFixture; import com.objectcomputing.checkins.services.fixture.VolunteeringFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; -import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpRequest; +import io.micronaut.context.annotation.Property; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; -import io.micronaut.http.client.BlockingHttpClient; -import io.micronaut.http.client.HttpClient; -import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientResponseException; import jakarta.inject.Inject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; @@ -26,28 +24,32 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class VolunteeringControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, VolunteeringFixture { +@Property(name = VolunteeringOrganizationClient.ENABLED, value = "true") +class VolunteeringOrganizationControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, VolunteeringFixture { @Inject - @Client("/services/volunteer") - HttpClient httpClient; - - BlockingHttpClient client; + VolunteeringOrganizationClient organizationClient; @BeforeEach void makeRoles() { - client = httpClient.toBlocking(); createAndAssignRoles(); } + static private String auth(String email, String role) { + return "Basic " + Base64.getEncoder().encodeToString((email + ":" + role).getBytes(StandardCharsets.UTF_8)); + } + @Test void testCreateOrganization() { MemberProfile memberProfile = createADefaultMemberProfile(); - List list = client.retrieve(HttpRequest.GET("/organization").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); + + List list = organizationClient.getAllOrganizations(memberAuth); assertEquals(0, list.size()); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); - HttpResponse response = client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE), VolunteeringOrganization.class); + HttpResponse response = organizationClient.createOrganization(adminAuth, org); assertEquals(HttpStatus.CREATED, response.getStatus()); assertNotNull(response.body().getId()); assertEquals("name", response.body().getName()); @@ -55,7 +57,7 @@ void testCreateOrganization() { assertEquals("website", response.body().getWebsite()); // List works as member without the profile - list = client.retrieve(HttpRequest.GET("/organization").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + list = organizationClient.getAllOrganizations(memberAuth); assertEquals(1, list.size()); assertEquals("name", list.getFirst().getName()); assertEquals("description", list.getFirst().getDescription()); @@ -66,47 +68,64 @@ void testCreateOrganization() { @Test void organizationsCanBeInactive() { MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); createVolunteeringOrganization("alpha", "alpha desc", "https://alpha.com"); createVolunteeringOrganization("gamma", "gamma desc", "https://gamma.com"); createVolunteeringOrganization("epsilon", "epsilon desc", "https://epsilon.com"); createVolunteeringOrganization("beta", "beta desc", "https://beta.com", false); // List by default hides inactive (and they're ordered by name) - List list = client.retrieve(HttpRequest.GET("/organization").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + List list = organizationClient.getAllOrganizations(memberAuth); assertEquals(3, list.size()); assertEquals(List.of("alpha", "epsilon", "gamma"), list.stream().map(VolunteeringOrganization::getName).toList()); // List with includeDeactivated shows all (and they're ordered by name) - list = client.retrieve(HttpRequest.GET("/organization?includeDeactivated=true").basicAuth(memberProfile.getWorkEmail(), MEMBER_ROLE), Argument.listOf(VolunteeringOrganization.class)); + list = organizationClient.getAllOrganizations(memberAuth, true); assertEquals(4, list.size()); assertEquals(List.of("alpha", "beta", "epsilon", "gamma"), list.stream().map(VolunteeringOrganization::getName).toList()); } @Test void testCreateOrganizationWithoutRole() { + String memberAuth = auth(MEMBER_ROLE, MEMBER_ROLE); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); - HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(MEMBER_ROLE, MEMBER_ROLE))); + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.createOrganization(memberAuth, org)); assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); } @Test void testCreateOrganizationWithoutDuplicateName() { MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); createVolunteeringOrganization("name", "description", "website"); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); - HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.createOrganization(adminAuth, org)); assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); assertEquals("Volunteering Organization with name name already exists", e.getMessage()); } + @Test + void cannotRenameAsDuplicateName() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); + + createVolunteeringOrganization("first", "desc", "web"); + VolunteeringOrganization second = createVolunteeringOrganization("second", "description", "website"); + + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.updateOrganization(adminAuth, second.getId(), new VolunteeringOrganizationDTO("first", "desc", "web"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Volunteering Organization with name first already exists", e.getMessage()); + } + @Test void testCreateOrganizationWithoutName() { MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO(null, "description", "website"); - HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.createOrganization(adminAuth, org)); assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); String body = e.getResponse().getBody(String.class).get(); assertTrue(body.contains("organization.name: must not be blank"), body + " should contain 'organization.name: must not be blank'"); @@ -115,9 +134,10 @@ void testCreateOrganizationWithoutName() { @Test void testCreateOrganizationWithoutDescription() { MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", null, "website"); - HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.createOrganization(adminAuth, org)); assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); String body = e.getResponse().getBody(String.class).get(); assertTrue(body.contains("organization.description: must not be blank"), body + " should contain 'organization.description: must not be blank'"); @@ -126,9 +146,10 @@ void testCreateOrganizationWithoutDescription() { @Test void testCreateOrganizationWithoutWebsite() { MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", null); - HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/organization", org).basicAuth(memberProfile.getWorkEmail(), ADMIN_ROLE))); + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.createOrganization(adminAuth, org)); assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); String body = e.getResponse().getBody(String.class).get(); assertTrue(body.contains("organization.website: must not be blank"), body + " should contain 'organization.website: must not be blank'"); From d658420d795fbac08aee27a90c34b0f720b8f5cf Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Mon, 10 Jun 2024 16:47:07 +0100 Subject: [PATCH 09/20] wip --- .../services/permissions/Permission.java | 2 +- .../volunteering/VolunteeringServiceImpl.java | 63 ++++++++++++++++--- .../resources/db/dev/R__Load_testing_data.sql | 2 +- .../services/fixture/PermissionFixture.java | 2 +- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java index 7fff2524e9..01a6802b87 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java @@ -51,7 +51,7 @@ public enum Permission { CAN_VIEW_ALL_PULSE_RESPONSES("View pulse responses", "Reporting"), CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS("Update volunteering organizations", "Volunteering"), CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS("Update volunteering relationships", "Volunteering"), - CAN_ADMINISTER_VOLUNTEERING_HOURS("Update volunteering hours", "Volunteering"); + CAN_ADMINISTER_VOLUNTEERING_EVENTS("Update volunteering events", "Volunteering"); private final String description; private final String category; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index cfa95046dc..9b8d37df62 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -1,6 +1,10 @@ package com.objectcomputing.checkins.services.volunteering; import com.objectcomputing.checkins.exceptions.BadArgException; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; +import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.role.role_permissions.RolePermissionServices; import io.micronaut.core.annotation.Nullable; import jakarta.inject.Singleton; import org.slf4j.Logger; @@ -15,15 +19,24 @@ class VolunteeringServiceImpl implements VolunteeringService { private static final Logger LOG = LoggerFactory.getLogger(VolunteeringServiceImpl.class); private static final String ORG_NAME_ALREADY_EXISTS_MESSAGE = "Volunteering Organization with name %s already exists"; + private final MemberProfileRepository memberProfileRepository; + private final CurrentUserServices currentUserServices; + private final RolePermissionServices rolePermissionServices; private final VolunteeringOrganizationRepository organizationRepo; private final VolunteeringRelationshipRepository relationshipRepo; private final VolunteeringEventRepository eventRepo; VolunteeringServiceImpl( + MemberProfileRepository memberProfileRepository, + CurrentUserServices currentUserServices, + RolePermissionServices rolePermissionServices, VolunteeringOrganizationRepository organizationRepo, VolunteeringRelationshipRepository relationshipRepo, VolunteeringEventRepository eventRepo ) { + this.memberProfileRepository = memberProfileRepository; + this.currentUserServices = currentUserServices; + this.rolePermissionServices = rolePermissionServices; this.organizationRepo = organizationRepo; this.relationshipRepo = relationshipRepo; this.eventRepo = eventRepo; @@ -71,13 +84,21 @@ public VolunteeringOrganization create(VolunteeringOrganization organization) { } @Override - public VolunteeringRelationship create(VolunteeringRelationship organization) { - return null; + public VolunteeringRelationship create(VolunteeringRelationship relationship) { + if (relationship.getId() != null) { + return update(relationship); + } + validateRelationship(relationship, "create"); + return relationshipRepo.save(relationship); } @Override - public VolunteeringEvent create(VolunteeringEvent organization) { - return null; + public VolunteeringEvent create(VolunteeringEvent event) { + if (event.getId() != null) { + return update(event); + } + validateEvent(event, "create"); + return eventRepo.save(event); } @Override @@ -91,15 +112,43 @@ public VolunteeringOrganization update(VolunteeringOrganization organization) { } @Override - public VolunteeringRelationship update(VolunteeringRelationship organization) { - return null; + public VolunteeringRelationship update(VolunteeringRelationship relationship) { + validateRelationship(relationship, "update"); + return relationshipRepo.update(relationship); } @Override - public VolunteeringEvent update(VolunteeringEvent organization) { + public VolunteeringEvent update(VolunteeringEvent event) { return null; } + private void validateRelationship(VolunteeringRelationship relationship, String action) { + validate(memberProfileRepository.findById(relationship.getMemberId()).isEmpty(), "Member %s doesn't exist", relationship.getMemberId()); + validate(organizationRepo.findById(relationship.getOrganizationId()).isEmpty(), "Organization %s doesn't exist", relationship.getOrganizationId()); + validatePermission(relationship, action); + } + + private void validateEvent(VolunteeringEvent event, String action) { + validate(relationshipRepo.findById(event.getRelationshipId()).isEmpty(), "Relationship %s doesn't exist", event.getRelationshipId()); + validate(event.getHours() < 0, "Hours must be non-negative"); + validatePermission(event, action); + } + + private void validatePermission(VolunteeringRelationship relationship, String action) { + // Fail if the user doesn't have permission to modify the relationship + UUID currentUserId = currentUserServices.getCurrentUser().getId(); + boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS); + validate(!hasPermission && !relationship.getMemberId().equals(currentUserId), "User %s does not have permission to %s Volunteering relationship for user %s", currentUserId, action, relationship.getMemberId()); + } + + private void validatePermission(VolunteeringEvent event, String action) { + // Fail if the user doesn't have permission to modify the event + UUID currentUserId = currentUserServices.getCurrentUser().getId(); + boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_VIEW_ALL_PULSE_RESPONSES); + boolean ownersRelationship = relationshipRepo.findById(event.getRelationshipId()).map(r -> r.getMemberId().equals(currentUserId)).orElse(false); + validate(!hasPermission && !ownersRelationship, "User %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, event.getRelationshipId()); + } + private void validate(boolean isError, String message, Object... args) { if (isError) { throw new BadArgException(String.format(message, args)); diff --git a/server/src/main/resources/db/dev/R__Load_testing_data.sql b/server/src/main/resources/db/dev/R__Load_testing_data.sql index 353713f690..447cbcb67b 100644 --- a/server/src/main/resources/db/dev/R__Load_testing_data.sql +++ b/server/src/main/resources/db/dev/R__Load_testing_data.sql @@ -993,7 +993,7 @@ values insert into role_permissions (roleid, permission) values - ('d03f5f0b-e29c-4cf4-9ea4-6baa09405c56', 'CAN_ADMINISTER_VOLUNTEERING_HOURS'); + ('d03f5f0b-e29c-4cf4-9ea4-6baa09405c56', 'CAN_ADMINISTER_VOLUNTEERING_EVENTS'); -- Member permissions insert into role_permissions diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java index a8a6d9682b..1a75cb596e 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java @@ -88,7 +88,7 @@ public interface PermissionFixture extends RolePermissionFixture { Permission.CAN_VIEW_ALL_PULSE_RESPONSES, Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS, Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS, - Permission.CAN_ADMINISTER_VOLUNTEERING_HOURS + Permission.CAN_ADMINISTER_VOLUNTEERING_EVENTS ); default void setPermissionsForAdmin(UUID roleID) { From fdf571f8f2f2a1646806c8d7dc0fd20fa4e99ab6 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 11:05:42 +0100 Subject: [PATCH 10/20] wip --- .../volunteering/VolunteeringController.java | 146 --------------- .../volunteering/VolunteeringEvent.java | 4 +- .../VolunteeringEventController.java | 40 ++++ .../VolunteeringOrganizationController.java | 83 ++++++++ .../VolunteeringOrganizationDTO.java | 2 +- .../VolunteeringOrganizationRepository.java | 1 - .../VolunteeringRelationship.java | 4 +- .../VolunteeringRelationshipController.java | 88 +++++++++ .../VolunteeringRelationshipDTO.java | 19 +- .../volunteering/VolunteeringServiceImpl.java | 4 +- .../V106__create_volunteering_tables.sql | 2 +- .../services/fixture/VolunteeringFixture.java | 12 ++ .../volunteering/VolunteeringClients.java | 53 ++++++ .../VolunteeringOrganizationClient.java | 34 ---- ...olunteeringOrganizationControllerTest.java | 40 ++-- ...olunteeringRelationshipControllerTest.java | 177 ++++++++++++++++++ 16 files changed, 493 insertions(+), 216 deletions(-) delete mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipController.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java delete mode 100644 server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java deleted file mode 100644 index a81bcb4019..0000000000 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringController.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.objectcomputing.checkins.services.volunteering; - -import com.objectcomputing.checkins.services.permissions.Permission; -import com.objectcomputing.checkins.services.permissions.RequiredPermission; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import io.micronaut.http.annotation.Put; -import io.micronaut.http.annotation.Status; -import io.micronaut.scheduling.TaskExecutors; -import io.micronaut.scheduling.annotation.ExecuteOn; -import io.micronaut.security.annotation.Secured; -import io.micronaut.security.rules.SecurityRule; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; - -import java.util.List; -import java.util.UUID; - -import static io.micronaut.http.HttpStatus.CREATED; - -@ExecuteOn(TaskExecutors.BLOCKING) -@Secured(SecurityRule.IS_AUTHENTICATED) -@Tag(name = "volunteering") -@Controller("/services/volunteer") -class VolunteeringController { - - private final VolunteeringService volunteeringService; - - public VolunteeringController(VolunteeringService volunteeringService) { - this.volunteeringService = volunteeringService; - } - - /** - * List all volunteering organizations - * - * @param includeDeactivated whether to include deactivated organizations - * @return list of {@link VolunteeringOrganization} - */ - @Get("/organization{?includeDeactivated}") - List findAll(@Nullable Boolean includeDeactivated) { - return volunteeringService.listOrganizations(Boolean.TRUE.equals(includeDeactivated)); - } - - /** - * Create a new volunteering organization - * - * @param organization the organization to create - * @return the created {@link VolunteeringOrganization} - */ - @Post("/organization") - @Status(CREATED) - @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) - VolunteeringOrganization create(@Valid @Body VolunteeringOrganizationDTO organization) { - return volunteeringService.create(new VolunteeringOrganization( - organization.getName(), - organization.getDescription(), - organization.getWebsite() - )); - } - - /** - * Update an existing volunteering organization - * - * @param organization the organization to update - * @return the updated {@link VolunteeringOrganization} - */ - @Put("/organization/{id}") - @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) - VolunteeringOrganization update(@NotNull UUID id, @Valid @Body VolunteeringOrganizationDTO organization) { - return volunteeringService.update(new VolunteeringOrganization( - id, - organization.getName(), - organization.getDescription(), - organization.getWebsite(), - Boolean.TRUE.equals(organization.getActive()) - )); - } - - /** - * Create a new volunteering relationship - * - * @param relationship the relationship to create - * @return the created {@link VolunteeringRelationship} - */ - @Post("/relationship") - VolunteeringRelationship create(@Valid @Body VolunteeringRelationshipDTO relationship) { - return volunteeringService.create(new VolunteeringRelationship( - relationship.getMemberId(), - relationship.getOrganizationId(), - relationship.getStartDate(), - relationship.getEndDate() - )); - } - - /** - * Update an existing volunteering relationship - * - * @param relationship the relationship to update - * @return the updated {@link VolunteeringRelationship} - */ - @Put("/relationship/{id}") - VolunteeringRelationship update(@NotNull UUID id, @Valid @Body VolunteeringRelationshipDTO relationship) { - return volunteeringService.update(new VolunteeringRelationship( - id, - relationship.getMemberId(), - relationship.getOrganizationId(), - relationship.getStartDate(), - relationship.getEndDate(), - Boolean.TRUE.equals(relationship.getActive()) - )); - } - - /** - * List all volunteering relationships - * If memberId is provided, restrict to relationships for that member. - * If organizationId is provided, restrict to relationships for that organization. - * If includeInactive is true, include inactive relationships and organizations in the results. - * - * @param memberId the id of the member - * @param organizationId the id of the organization - * @param includeDeactivated whether to include deactivated relationships or organizations - * @return list of {@link VolunteeringRelationship} - */ - @Get("/relationship/{?memberId,organizationId,includeDeactivated}") - List findRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated) { - return volunteeringService.listRelationships(memberId, organizationId, Boolean.TRUE.equals(includeDeactivated)); - } - - /** - * List all volunteering events. - * If relationshipId is provided, restrict to events for that relationship. - * If includeInactive is true, include inactive organizations and relationships in the results. - * - * @param relationshipId the id of the relationship - * @param includeDeactivated whether to include deactivated relationships or organizations - * @return list of {@link VolunteeringEvent} - */ - @Get("/event/{?relationshipId,includeDeactivated}") - List findEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, @Nullable Boolean includeDeactivated) { - return volunteeringService.listEvents(memberId, relationshipId, Boolean.TRUE.equals(includeDeactivated)); - } -} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java index 7cdd8a2add..a2276f1a88 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java @@ -3,11 +3,11 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.objectcomputing.checkins.converter.LocalDateConverter; import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.AutoPopulated; import io.micronaut.data.annotation.TypeDef; import io.micronaut.data.model.DataType; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -51,7 +51,7 @@ public class VolunteeringEvent { private int hours; @Nullable - @Column(name = "hours") + @Column(name = "notes") @TypeDef(type = DataType.STRING) @Schema(description = "notes about the volunteering event") private String notes; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java new file mode 100644 index 0000000000..9a035f6a55 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java @@ -0,0 +1,40 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; +import java.util.UUID; + +@ExecuteOn(TaskExecutors.BLOCKING) +@Secured(SecurityRule.IS_AUTHENTICATED) +@Tag(name = "volunteering") +@Controller("/services/volunteer/event") +class VolunteeringEventController { + + private final VolunteeringService volunteeringService; + + VolunteeringEventController(VolunteeringService volunteeringService) { + this.volunteeringService = volunteeringService; + } + + /** + * List all volunteering events. + * If relationshipId is provided, restrict to events for that relationship. + * If includeInactive is true, include inactive organizations and relationships in the results. + * + * @param relationshipId the id of the relationship + * @param includeDeactivated whether to include deactivated relationships or organizations + * @return list of {@link VolunteeringEvent} + */ + @Get("/{?relationshipId,includeDeactivated}") + List findEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, @Nullable Boolean includeDeactivated) { + return volunteeringService.listEvents(memberId, relationshipId, Boolean.TRUE.equals(includeDeactivated)); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java new file mode 100644 index 0000000000..bd629ba6ec --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java @@ -0,0 +1,83 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.permissions.RequiredPermission; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.annotation.Status; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +import static io.micronaut.http.HttpStatus.CREATED; + +@ExecuteOn(TaskExecutors.BLOCKING) +@Secured(SecurityRule.IS_AUTHENTICATED) +@Tag(name = "volunteering") +@Controller("/services/volunteer/organization") +class VolunteeringOrganizationController { + + private final VolunteeringService volunteeringService; + + VolunteeringOrganizationController(VolunteeringService volunteeringService) { + this.volunteeringService = volunteeringService; + } + + /** + * List all volunteering organizations. + * + * @param includeDeactivated whether to include deactivated organizations + * @return list of {@link VolunteeringOrganization} + */ + @Get("/{?includeDeactivated}") + List findAll(@Nullable Boolean includeDeactivated) { + return volunteeringService.listOrganizations(Boolean.TRUE.equals(includeDeactivated)); + } + + /** + * Create a new volunteering organization. + * + * @param organization the organization to create + * @return the created {@link VolunteeringOrganization} + */ + @Post + @Status(CREATED) + VolunteeringOrganization create(@Valid @Body VolunteeringOrganizationDTO organization) { + return volunteeringService.create(new VolunteeringOrganization( + organization.getName(), + organization.getDescription(), + organization.getWebsite() + )); + } + + /** + * Update an existing volunteering organization. + * Requires the {@link Permission#CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS} permission. + * + * @param id the id of the organization to update + * @param organization the organization to update + * @return the updated {@link VolunteeringOrganization} + */ + @Put("/{id}") +// @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) + VolunteeringOrganization update(@NotNull UUID id, @Valid @Body VolunteeringOrganizationDTO organization) { + return volunteeringService.update(new VolunteeringOrganization( + id, + organization.getName(), + organization.getDescription(), + organization.getWebsite(), + Boolean.TRUE.equals(organization.getActive()) + )); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java index 14ed90e07c..7e3217eeec 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationDTO.java @@ -28,7 +28,7 @@ public class VolunteeringOrganizationDTO { @Schema(description = "whether the Volunteering Organization is active") private Boolean active; - public VolunteeringOrganizationDTO(String name, String description, String website) { + public VolunteeringOrganizationDTO(@NotBlank String name, @NotBlank String description, @NotBlank String website) { this(name, description, website, true); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java index ac2bb56441..d44c96d1f9 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationRepository.java @@ -1,6 +1,5 @@ package com.objectcomputing.checkins.services.volunteering; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.data.annotation.Query; import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java index 5987554f3d..0cfe5390bb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java @@ -3,11 +3,11 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.objectcomputing.checkins.converter.LocalDateConverter; import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.AutoPopulated; import io.micronaut.data.annotation.TypeDef; import io.micronaut.data.model.DataType; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -67,7 +67,7 @@ public class VolunteeringRelationship { @Schema(description = "whether the Volunteering Relationship is active") private boolean active = true; - VolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, LocalDate endDate) { + public VolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, @Nullable LocalDate endDate) { this(null, memberId, organizationId, startDate, endDate, true); } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipController.java new file mode 100644 index 0000000000..d68844cb57 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipController.java @@ -0,0 +1,88 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.objectcomputing.checkins.services.permissions.Permission; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.annotation.Status; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +import static io.micronaut.http.HttpStatus.CREATED; + +@ExecuteOn(TaskExecutors.BLOCKING) +@Secured(SecurityRule.IS_AUTHENTICATED) +@Tag(name = "volunteering") +@Controller("/services/volunteer/relationship") +class VolunteeringRelationshipController { + + private final VolunteeringService volunteeringService; + + VolunteeringRelationshipController(VolunteeringService volunteeringService) { + this.volunteeringService = volunteeringService; + } + + /** + * List all volunteering relationships + * If memberId is provided, restrict to relationships for that member. + * If organizationId is provided, restrict to relationships for that organization. + * If includeInactive is true, include inactive relationships and organizations in the results. + * + * @param memberId the id of the member + * @param organizationId the id of the organization + * @param includeDeactivated whether to include deactivated relationships or organizations + * @return list of {@link VolunteeringRelationship} + */ + @Get("/{?memberId,organizationId,includeDeactivated}") + List findRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated) { + return volunteeringService.listRelationships(memberId, organizationId, Boolean.TRUE.equals(includeDeactivated)); + } + + /** + * Create a new volunteering relationship. + * + * @param relationship the relationship to create + * @return the created {@link VolunteeringRelationship} + */ + @Post + @Status(CREATED) + VolunteeringRelationship create(@Valid @Body VolunteeringRelationshipDTO relationship) { + return volunteeringService.create(new VolunteeringRelationship( + relationship.getMemberId(), + relationship.getOrganizationId(), + relationship.getStartDate(), + relationship.getEndDate() + )); + } + + /** + * Update an existing volunteering relationship. + * Requires you to be the creator of the relationship, or the {@link Permission#CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS} permission. + * + * @param id the id of the relationship to update + * @param relationship the relationship to update + * @return the updated {@link VolunteeringRelationship} + */ + @Put("/{id}") + VolunteeringRelationship update(@NotNull UUID id, @Valid @Body VolunteeringRelationshipDTO relationship) { + return volunteeringService.update(new VolunteeringRelationship( + id, + relationship.getMemberId(), + relationship.getOrganizationId(), + relationship.getStartDate(), + relationship.getEndDate(), + Boolean.TRUE.equals(relationship.getActive()) + )); + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java index 2f85a50ae1..36d65f951f 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipDTO.java @@ -1,18 +1,11 @@ package com.objectcomputing.checkins.services.volunteering; import com.fasterxml.jackson.annotation.JsonFormat; -import com.objectcomputing.checkins.converter.LocalDateConverter; import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.data.annotation.AutoPopulated; -import io.micronaut.data.annotation.TypeDef; -import io.micronaut.data.model.DataType; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -21,6 +14,7 @@ @Setter @Getter +@AllArgsConstructor @Introspected public class VolunteeringRelationshipDTO { @@ -42,6 +36,11 @@ public class VolunteeringRelationshipDTO { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private LocalDate endDate; + @Nullable @Schema(description = "whether the Volunteering Relationship is active") - private Boolean active = true; + private Boolean active; + + public VolunteeringRelationshipDTO(@NotNull UUID memberId, @NotNull UUID organizationId, @NotNull LocalDate startDate, @Nullable LocalDate endDate) { + this(memberId, organizationId, startDate, endDate, true); + } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index 9b8d37df62..40b7f3c33d 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -138,7 +138,7 @@ private void validatePermission(VolunteeringRelationship relationship, String ac // Fail if the user doesn't have permission to modify the relationship UUID currentUserId = currentUserServices.getCurrentUser().getId(); boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS); - validate(!hasPermission && !relationship.getMemberId().equals(currentUserId), "User %s does not have permission to %s Volunteering relationship for user %s", currentUserId, action, relationship.getMemberId()); + validate(!hasPermission && !relationship.getMemberId().equals(currentUserId), "Member %s does not have permission to %s Volunteering relationship for member %s", currentUserId, action, relationship.getMemberId()); } private void validatePermission(VolunteeringEvent event, String action) { @@ -146,7 +146,7 @@ private void validatePermission(VolunteeringEvent event, String action) { UUID currentUserId = currentUserServices.getCurrentUser().getId(); boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_VIEW_ALL_PULSE_RESPONSES); boolean ownersRelationship = relationshipRepo.findById(event.getRelationshipId()).map(r -> r.getMemberId().equals(currentUserId)).orElse(false); - validate(!hasPermission && !ownersRelationship, "User %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, event.getRelationshipId()); + validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, event.getRelationshipId()); } private void validate(boolean isError, String message, Object... args) { diff --git a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql index b2c06c4b5a..98a505aa94 100644 --- a/server/src/main/resources/db/common/V106__create_volunteering_tables.sql +++ b/server/src/main/resources/db/common/V106__create_volunteering_tables.sql @@ -18,7 +18,7 @@ CREATE TABLE volunteering_relationship organization_id varchar REFERENCES volunteering_organization (organization_id), start_date timestamp NOT NULL, end_date timestamp, - is_active boolean NOT NULL DEFAULT TRUE + is_active boolean NOT NULL DEFAULT TRUE ); CREATE TABLE volunteering_event diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java index 1691ebff47..a0d9fcab57 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java @@ -1,6 +1,10 @@ package com.objectcomputing.checkins.services.fixture; import com.objectcomputing.checkins.services.volunteering.VolunteeringOrganization; +import com.objectcomputing.checkins.services.volunteering.VolunteeringRelationship; + +import java.time.LocalDate; +import java.util.UUID; public interface VolunteeringFixture extends RepositoryFixture { @@ -19,4 +23,12 @@ default VolunteeringOrganization createVolunteeringOrganization(String name, Str default VolunteeringOrganization createVolunteeringOrganization(String name, String description, String website, boolean active) { return getVolunteeringOrganizationRepository().save(new VolunteeringOrganization(name, description, website, active)); } + + default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate) { + return createVolunteeringRelationship(memberId, organizationId, startDate, null); + } + + default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, LocalDate endDate) { + return getVolunteeringRelationshipRepository().save(new VolunteeringRelationship(memberId, organizationId, startDate, endDate)); + } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java new file mode 100644 index 0000000000..d14a2b983a --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java @@ -0,0 +1,53 @@ +package com.objectcomputing.checkins.services.volunteering; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Header; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.client.annotation.Client; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.UUID; + +class VolunteeringClients { + + @Client("/services/volunteer/organization") + @Requires(property = VolunteeringClients.Organization.ENABLED, value = "true") + interface Organization { + + String ENABLED = "enable.volunteering.organization.client"; + + @Get("/") + List list(@Header String authorization); + + @Get("/{?includeDeactivated}") + List list(@Header String authorization, @Nullable Boolean includeDeactivated); + + @Post + HttpResponse createOrganization(@Header String authorization, @Body VolunteeringOrganizationDTO organization); + + @Put("/{id}") + VolunteeringOrganization updateOrganization(@Header String authorization, @NotNull UUID id, @Body VolunteeringOrganizationDTO organization); + } + + @Client("/services/volunteer/relationship") + @Requires(property = VolunteeringClients.Relationship.ENABLED, value = "true") + interface Relationship { + + String ENABLED = "enable.volunteering.relationship.client"; + + @Get("/{?memberId,organizationId,includeDeactivated}") + List list(@Header String authorization, @Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated); + + @Post + HttpResponse create(@Header String authorization, @Body VolunteeringRelationshipDTO relationship); + + @Put("/{id}") + VolunteeringRelationship update(@Header String authorization, @NotNull UUID id, @Body VolunteeringRelationshipDTO relationship); + } +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java deleted file mode 100644 index a0a5c2e51d..0000000000 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationClient.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.objectcomputing.checkins.services.volunteering; - -import io.micronaut.context.annotation.Requires; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Header; -import io.micronaut.http.annotation.Post; -import io.micronaut.http.annotation.Put; -import io.micronaut.http.client.annotation.Client; -import jakarta.validation.constraints.NotNull; - -import java.util.List; -import java.util.UUID; - -@Client("/services/volunteer/organization") -@Requires(property = VolunteeringOrganizationClient.ENABLED, value = "true") -public interface VolunteeringOrganizationClient { - - String ENABLED = "enable.volunteering.organization.client"; - - @Get("/") - List getAllOrganizations(@Header String authorization); - - @Get("/{?includeDeactivated}") - List getAllOrganizations(@Header String authorization, @Nullable Boolean includeDeactivated); - - @Post - HttpResponse createOrganization(@Header String authorization, @Body VolunteeringOrganizationDTO organization); - - @Put("/{id}") - VolunteeringOrganization updateOrganization(@Header String authorization, @NotNull UUID id, @Body VolunteeringOrganizationDTO organization); -} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java index bfa6cb20d8..462de10c14 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationControllerTest.java @@ -24,32 +24,37 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -@Property(name = VolunteeringOrganizationClient.ENABLED, value = "true") +@Property(name = VolunteeringClients.Organization.ENABLED, value = "true") class VolunteeringOrganizationControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, VolunteeringFixture { @Inject - VolunteeringOrganizationClient organizationClient; + VolunteeringClients.Organization organizationClient; + + static private String auth(String email, String role) { + return "Basic " + Base64.getEncoder().encodeToString((email + ":" + role).getBytes(StandardCharsets.UTF_8)); + } @BeforeEach void makeRoles() { createAndAssignRoles(); } - static private String auth(String email, String role) { - return "Basic " + Base64.getEncoder().encodeToString((email + ":" + role).getBytes(StandardCharsets.UTF_8)); + @Test + void startsEmpty() { + var list = organizationClient.list(auth(MEMBER_ROLE, MEMBER_ROLE)); + assertTrue(list.isEmpty()); } @Test void testCreateOrganization() { MemberProfile memberProfile = createADefaultMemberProfile(); String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); - String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); - List list = organizationClient.getAllOrganizations(memberAuth); + List list = organizationClient.list(memberAuth); assertEquals(0, list.size()); VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); - HttpResponse response = organizationClient.createOrganization(adminAuth, org); + HttpResponse response = organizationClient.createOrganization(memberAuth, org); assertEquals(HttpStatus.CREATED, response.getStatus()); assertNotNull(response.body().getId()); assertEquals("name", response.body().getName()); @@ -57,7 +62,7 @@ void testCreateOrganization() { assertEquals("website", response.body().getWebsite()); // List works as member without the profile - list = organizationClient.getAllOrganizations(memberAuth); + list = organizationClient.list(memberAuth); assertEquals(1, list.size()); assertEquals("name", list.getFirst().getName()); assertEquals("description", list.getFirst().getDescription()); @@ -75,27 +80,28 @@ void organizationsCanBeInactive() { createVolunteeringOrganization("beta", "beta desc", "https://beta.com", false); // List by default hides inactive (and they're ordered by name) - List list = organizationClient.getAllOrganizations(memberAuth); + List list = organizationClient.list(memberAuth); assertEquals(3, list.size()); assertEquals(List.of("alpha", "epsilon", "gamma"), list.stream().map(VolunteeringOrganization::getName).toList()); // List with includeDeactivated shows all (and they're ordered by name) - list = organizationClient.getAllOrganizations(memberAuth, true); + list = organizationClient.list(memberAuth, true); assertEquals(4, list.size()); assertEquals(List.of("alpha", "beta", "epsilon", "gamma"), list.stream().map(VolunteeringOrganization::getName).toList()); } @Test - void testCreateOrganizationWithoutRole() { - String memberAuth = auth(MEMBER_ROLE, MEMBER_ROLE); - VolunteeringOrganizationDTO org = new VolunteeringOrganizationDTO("name", "description", "website"); - - HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.createOrganization(memberAuth, org)); + void cannotUpdateWithoutPermission() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + var org = createVolunteeringOrganization("name", "description", "website"); + var update = new VolunteeringOrganizationDTO(org.getName(), "new description", "new website"); + HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> organizationClient.updateOrganization(memberAuth, org.getId(), update)); assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); } @Test - void testCreateOrganizationWithoutDuplicateName() { + void cannotCreateDuplicateNamedOrganization() { MemberProfile memberProfile = createADefaultMemberProfile(); String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); createVolunteeringOrganization("name", "description", "website"); @@ -107,7 +113,7 @@ void testCreateOrganizationWithoutDuplicateName() { } @Test - void cannotRenameAsDuplicateName() { + void cannotRenameWithDuplicateName() { MemberProfile memberProfile = createADefaultMemberProfile(); String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java new file mode 100644 index 0000000000..5034dd48ef --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java @@ -0,0 +1,177 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +import com.objectcomputing.checkins.services.fixture.VolunteeringFixture; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.Base64; +import java.util.List; + +import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; +import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Property(name = VolunteeringClients.Relationship.ENABLED, value = "true") +class VolunteeringRelationshipControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, VolunteeringFixture { + + @Inject + VolunteeringClients.Relationship relationshipClient; + + private final static String MEMBER_AUTH = auth(MEMBER_ROLE, MEMBER_ROLE); + private final static String ADMIN_AUTH = auth(ADMIN_ROLE, ADMIN_ROLE); + + static private String auth(String email, String role) { + return "Basic " + Base64.getEncoder().encodeToString((email + ":" + role).getBytes(StandardCharsets.UTF_8)); + } + + @BeforeEach + void makeRoles() { + createAndAssignRoles(); + } + + @Test + void startsEmpty() { + var list = relationshipClient.list(MEMBER_AUTH, null, null, null); + assertTrue(list.isEmpty()); + } + + @Test + void memberCanCreateRelationshipForSelf() { + MemberProfile memberProfile = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + LocalDate startDate = LocalDate.now(); + + var relationship = new VolunteeringRelationshipDTO(memberProfile.getId(), organization.getId(), startDate, null); + var createdRelationship = relationshipClient.create(auth(memberProfile.getWorkEmail(), MEMBER_ROLE), relationship); + assertEquals(HttpStatus.CREATED, createdRelationship.getStatus()); + var createdRelationshipBody = createdRelationship.body(); + assertNotNull(createdRelationshipBody.getId()); + + var list = relationshipClient.list(MEMBER_AUTH, null, null, null); + assertEquals(1, list.size()); + var first = list.getFirst(); + assertEquals(memberProfile.getId(), first.getMemberId()); + assertEquals(relationship.getMemberId(), first.getMemberId()); + assertEquals(organization.getId(), first.getOrganizationId()); + assertEquals(startDate, first.getStartDate()); + assertNull(first.getEndDate()); + assertTrue(first.isActive()); + } + + @Test + void memberCannotCreateRelationshipForSomeoneElse() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + LocalDate startDate = LocalDate.now(); + + var relationship = new VolunteeringRelationshipDTO(sarah.getId(), organization.getId(), startDate, null); + var e = assertThrows(HttpClientResponseException.class, () -> relationshipClient.create(memberAuth, relationship)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Member %s does not have permission to create Volunteering relationship for member %s".formatted(memberProfile.getId(), sarah.getId()), e.getMessage()); + } + + @Test + void adminCanCreateRelationshipForAnyone() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + LocalDate startDate = LocalDate.now(); + + var relationship = new VolunteeringRelationshipDTO(sarah.getId(), organization.getId(), startDate, null); + var created = relationshipClient.create(adminAuth, relationship); + assertEquals(HttpStatus.CREATED, created.getStatus()); + var createdBody = created.body(); + assertNotNull(createdBody.getId()); + assertEquals(createdBody.getMemberId(), sarah.getId()); + assertEquals(createdBody.getOrganizationId(), organization.getId()); + assertEquals(createdBody.getStartDate(), startDate); + assertNull(createdBody.getEndDate()); + assertTrue(createdBody.isActive()); + } + + @Test + void cannotUpdateOtherMembersRelationshipsWithoutPermission() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + + LocalDate startDate = LocalDate.now(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(sarah.getId(), organization.getId(), startDate); + + VolunteeringRelationshipDTO updateDto = new VolunteeringRelationshipDTO(relationship.getMemberId(), relationship.getOrganizationId(), startDate.plusDays(1), LocalDate.now()); + + var update = assertThrows(HttpClientResponseException.class, () -> relationshipClient.update(memberAuth, relationship.getId(), updateDto)); + assertEquals(HttpStatus.BAD_REQUEST, update.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering relationship for member %s".formatted(memberProfile.getId(), sarah.getId()), update.getMessage()); + } + + @Test + void canUpdateOtherMembersRelationshipsWithPermission() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String adminAuth = auth(memberProfile.getWorkEmail(), ADMIN_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + + LocalDate startDate = LocalDate.now(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(sarah.getId(), organization.getId(), startDate); + + VolunteeringRelationshipDTO updateDto = new VolunteeringRelationshipDTO(relationship.getMemberId(), relationship.getOrganizationId(), startDate.plusDays(1), startDate.plusDays(3), false); + + VolunteeringRelationship updated = relationshipClient.update(adminAuth, relationship.getId(), updateDto); + assertEquals(relationship.getId(), updated.getId()); + assertEquals(sarah.getId(), updated.getMemberId()); + assertEquals(organization.getId(), updated.getOrganizationId()); + assertEquals(startDate.plusDays(1), updated.getStartDate()); + assertEquals(startDate.plusDays(3), updated.getEndDate()); + assertFalse(updated.isActive()); + } + + @Test + void canUpdateYourOwnRelationshipsWithoutPermission() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + + LocalDate startDate = LocalDate.now(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(memberProfile.getId(), organization.getId(), startDate); + + VolunteeringRelationshipDTO updateDto = new VolunteeringRelationshipDTO(relationship.getMemberId(), relationship.getOrganizationId(), startDate.plusDays(1), startDate.plusDays(3)); + + VolunteeringRelationship updated = relationshipClient.update(memberAuth, relationship.getId(), updateDto); + assertEquals(relationship.getId(), updated.getId()); + assertEquals(memberProfile.getId(), updated.getMemberId()); + assertEquals(organization.getId(), updated.getOrganizationId()); + assertEquals(startDate.plusDays(1), updated.getStartDate()); + assertEquals(startDate.plusDays(3), updated.getEndDate()); + assertTrue(updated.isActive()); + } +} From 7e03de5c51d4c3a7dc716987d3d2d18609eba16d Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 11:34:26 +0100 Subject: [PATCH 11/20] Test relationship filtering --- .../VolunteeringRelationship.java | 4 ++ .../services/fixture/VolunteeringFixture.java | 12 +++- ...olunteeringRelationshipControllerTest.java | 58 ++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java index 0cfe5390bb..107aa6d8b6 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationship.java @@ -70,4 +70,8 @@ public class VolunteeringRelationship { public VolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, @Nullable LocalDate endDate) { this(null, memberId, organizationId, startDate, endDate, true); } + + public VolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, @Nullable LocalDate endDate, boolean active) { + this(null, memberId, organizationId, startDate, endDate, active); + } } \ No newline at end of file diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java index a0d9fcab57..cfb77b2bfe 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java @@ -25,10 +25,18 @@ default VolunteeringOrganization createVolunteeringOrganization(String name, Str } default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate) { - return createVolunteeringRelationship(memberId, organizationId, startDate, null); + return createVolunteeringRelationship(memberId, organizationId, startDate, null, true); + } + + default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, boolean active) { + return createVolunteeringRelationship(memberId, organizationId, startDate, null, active); } default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, LocalDate endDate) { - return getVolunteeringRelationshipRepository().save(new VolunteeringRelationship(memberId, organizationId, startDate, endDate)); + return createVolunteeringRelationship(memberId, organizationId, startDate, endDate, true); + } + + default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, LocalDate endDate, boolean active) { + return getVolunteeringRelationshipRepository().save(new VolunteeringRelationship(memberId, organizationId, startDate, endDate, active)); } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java index 5034dd48ef..cf67e33e65 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java @@ -6,7 +6,6 @@ import com.objectcomputing.checkins.services.fixture.VolunteeringFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import io.micronaut.context.annotation.Property; -import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientResponseException; import jakarta.inject.Inject; @@ -174,4 +173,61 @@ void canUpdateYourOwnRelationshipsWithoutPermission() { assertEquals(startDate.plusDays(3), updated.getEndDate()); assertTrue(updated.isActive()); } + + @Test + void canListAndFilterRelationships() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + MemberProfile tim = memberWithoutBoss("tim"); + + VolunteeringOrganization liftForLife = createVolunteeringOrganization("Lift for Life", "Educate, empower, uplift", "https://www.liftforlifeacademy.org"); + VolunteeringOrganization foodbank = createVolunteeringOrganization("St. Louis Area Foodbank", "Works with over 600 partners", "https://stlfoodbank.org/find-food/"); + + VolunteeringRelationship sarahLiftForLife = createVolunteeringRelationship(sarah.getId(), liftForLife.getId(), LocalDate.now().minusDays(2)); + VolunteeringRelationship timLiftForLife = createVolunteeringRelationship(tim.getId(), liftForLife.getId(), LocalDate.now()); + VolunteeringRelationship timFoodbankInactive = createVolunteeringRelationship(tim.getId(), foodbank.getId(), LocalDate.now().minusDays(3), null, false); + VolunteeringRelationship sarahFoodbank = createVolunteeringRelationship(sarah.getId(), foodbank.getId(), LocalDate.now().minusDays(10), LocalDate.now()); + + // Can find all relationships in correct order + List allRelationships = relationshipClient.list(memberAuth, null, null, null); + assertEquals(3, allRelationships.size()); + assertEquals(List.of(sarahFoodbank.getId(), sarahLiftForLife.getId(), timLiftForLife.getId()), allRelationships.stream().map(VolunteeringRelationship::getId).toList()); + + // Can include inactive relationships + List allWithInactiveRelationships = relationshipClient.list(memberAuth, null, null, true); + assertEquals(4, allWithInactiveRelationships.size()); + assertEquals(List.of(sarahFoodbank.getId(), timFoodbankInactive.getId(), sarahLiftForLife.getId(), timLiftForLife.getId()), allWithInactiveRelationships.stream().map(VolunteeringRelationship::getId).toList()); + + // Can filter by memberId + List timRelationships = relationshipClient.list(memberAuth, tim.getId(), null, null); + assertEquals(1, timRelationships.size()); + assertEquals(List.of(timLiftForLife.getId()), timRelationships.stream().map(VolunteeringRelationship::getId).toList()); + + // Can filter by organization + List liftRelationships = relationshipClient.list(memberAuth, null, liftForLife.getId(), null); + assertEquals(2, liftRelationships.size()); + assertEquals(List.of(sarahLiftForLife.getId(), timLiftForLife.getId()), liftRelationships.stream().map(VolunteeringRelationship::getId).toList()); + + // Can filter by organization and inactive + List foodRelationships = relationshipClient.list(memberAuth, null, foodbank.getId(), null); + assertEquals(1, foodRelationships.size()); + assertEquals(List.of(sarahFoodbank.getId()), foodRelationships.stream().map(VolunteeringRelationship::getId).toList()); + foodRelationships = relationshipClient.list(memberAuth, null, foodbank.getId(), true); + assertEquals(2, foodRelationships.size()); + assertEquals(List.of(sarahFoodbank.getId(), timFoodbankInactive.getId()), foodRelationships.stream().map(VolunteeringRelationship::getId).toList()); + + // Can filter by member and organization + List sarahLiftRelationships = relationshipClient.list(memberAuth, sarah.getId(), liftForLife.getId(), null); + assertEquals(1, sarahLiftRelationships.size()); + assertEquals(List.of(sarahLiftForLife.getId()), sarahLiftRelationships.stream().map(VolunteeringRelationship::getId).toList()); + + // Can filter by member and organization and inactive + List timFood = relationshipClient.list(memberAuth, tim.getId(), foodbank.getId(), null); + assertEquals(0, timFood.size()); + timFood = relationshipClient.list(memberAuth, tim.getId(), foodbank.getId(), true); + assertEquals(1, timFood.size()); + assertEquals(List.of(timFoodbankInactive.getId()), timFood.stream().map(VolunteeringRelationship::getId).toList()); + } } From 5b58670b7a183c15a09f86721f1f88f893afec1c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 11:53:01 +0100 Subject: [PATCH 12/20] First pass at events --- .../volunteering/VolunteeringEvent.java | 16 +++++ .../VolunteeringEventController.java | 70 +++++++++++++++++-- .../volunteering/VolunteeringEventDTO.java | 49 +++++++++++++ .../VolunteeringEventRepository.java | 16 ++++- .../volunteering/VolunteeringService.java | 4 +- .../volunteering/VolunteeringServiceImpl.java | 28 +++++--- ...olunteeringRelationshipControllerTest.java | 1 - 7 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java index a2276f1a88..8a5af7e1be 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java @@ -13,7 +13,9 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDate; @@ -22,6 +24,8 @@ @Setter @Getter @Entity +@AllArgsConstructor +@NoArgsConstructor @Introspected @Table(name = "volunteering_event") public class VolunteeringEvent { @@ -55,4 +59,16 @@ public class VolunteeringEvent { @TypeDef(type = DataType.STRING) @Schema(description = "notes about the volunteering event") private String notes; + + public VolunteeringEvent(UUID relationshipId, LocalDate eventDate, int hours) { + this(null, relationshipId, eventDate, hours, null); + } + + public VolunteeringEvent(UUID relationshipId, LocalDate eventDate) { + this(null, relationshipId, eventDate, 0, null); + } + + public VolunteeringEvent(UUID relationshipId, LocalDate eventDate, int hours, String notes) { + this(null, relationshipId, eventDate, hours, notes); + } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java index 9a035f6a55..7a219f7669 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java @@ -1,17 +1,28 @@ package com.objectcomputing.checkins.services.volunteering; +import com.objectcomputing.checkins.services.permissions.Permission; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Delete; import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Put; +import io.micronaut.http.annotation.Status; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; +import static io.micronaut.http.HttpStatus.CREATED; + @ExecuteOn(TaskExecutors.BLOCKING) @Secured(SecurityRule.IS_AUTHENTICATED) @Tag(name = "volunteering") @@ -26,15 +37,66 @@ class VolunteeringEventController { /** * List all volunteering events. + * If memberId is provided, restrict to events for that member. * If relationshipId is provided, restrict to events for that relationship. * If includeInactive is true, include inactive organizations and relationships in the results. * - * @param relationshipId the id of the relationship + * @param memberId the id of the member + * @param organizationId the id of the organization * @param includeDeactivated whether to include deactivated relationships or organizations * @return list of {@link VolunteeringEvent} */ - @Get("/{?relationshipId,includeDeactivated}") - List findEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, @Nullable Boolean includeDeactivated) { - return volunteeringService.listEvents(memberId, relationshipId, Boolean.TRUE.equals(includeDeactivated)); + @Get("/{?memberId,relationshipId,includeDeactivated}") + List findEvents(@Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated) { + return volunteeringService.listEvents(memberId, organizationId, Boolean.TRUE.equals(includeDeactivated)); + } + + /** + * Create a new volunteering event. + * Requires you to be the creator of the relationship, or have the {@link Permission#CAN_ADMINISTER_VOLUNTEERING_EVENTS} permission. + * + * @param event the event to create + * @return the created {@link VolunteeringEvent} + */ + @Post + @Status(CREATED) + VolunteeringEvent create(@Valid @Body VolunteeringEventDTO event) { + return volunteeringService.create(new VolunteeringEvent( + event.getRelationshipId(), + event.getEventDate(), + event.getHours(), + event.getNotes() + )); + } + + /** + * Update an existing volunteering event. + * Requires you to be the creator of the relationship, or have the {@link Permission#CAN_ADMINISTER_VOLUNTEERING_EVENTS} permission. + * + * @param id the id of the relationship to update + * @param event the relationship to update + * @return the updated {@link VolunteeringEvent} + */ + @Put("/{id}") + VolunteeringEvent update(@NotNull UUID id, @Valid @Body VolunteeringEventDTO event) { + return volunteeringService.update(new VolunteeringEvent( + id, + event.getRelationshipId(), + event.getEventDate(), + event.getHours(), + event.getNotes() + )); + } + + /** + * Delete a volunteering event. + * Requires you to be the creator of the relationship, or have the {@link Permission#CAN_ADMINISTER_VOLUNTEERING_EVENTS} permission. + * + * @param id the id of the event to delete + */ + @Delete("/{id}") + @Status(HttpStatus.OK) + void delete(UUID id) { + volunteeringService.deleteEvent(id); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java new file mode 100644 index 0000000000..5e70469306 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java @@ -0,0 +1,49 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.objectcomputing.checkins.converter.LocalDateConverter; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.data.annotation.AutoPopulated; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.UUID; + +@Setter +@Getter +@AllArgsConstructor +@Introspected +public class VolunteeringEventDTO { + + @NotNull + @Schema(description = "id of the Volunteering relationship") + private UUID relationshipId; + + @NotNull + @Schema(description = "when the volunteering event occurred") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate eventDate; + + @Schema(description = "number of hours spent volunteering") + private int hours; + + @Nullable + @Schema(description = "notes about the volunteering event") + private String notes; + + public VolunteeringEventDTO(@NotNull UUID relationshipId, @NotNull LocalDate eventDate, int hours) { + this(relationshipId, eventDate, hours, null); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java index e858d28fd0..c8f6d72443 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java @@ -27,11 +27,23 @@ LEFT JOIN volunteering_organization AS org USING(organization_id) FROM volunteering_event AS event LEFT JOIN volunteering_relationship AS rel USING(relationship_id) LEFT JOIN volunteering_organization AS org USING(organization_id) - WHERE rel.relationship_id = :relationshipId + WHERE org.organization_id = :organizationId AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) AND (org.is_active = TRUE OR :includeDeactivated = TRUE) ORDER BY event.event_date, org.name""") - List findByRelationshipId(UUID relationshipId, boolean includeDeactivated); + List findByOrganizationId(UUID organizationId, boolean includeDeactivated); + + @Query(""" + SELECT event.* + FROM volunteering_event AS event + LEFT JOIN volunteering_relationship AS rel USING(relationship_id) + LEFT JOIN volunteering_organization AS org USING(organization_id) + WHERE rel.member_id = :memberId + AND org.organization_id = :organizationId + AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) + AND (org.is_active = TRUE OR :includeDeactivated = TRUE) + ORDER BY event.event_date, org.name""") + List findByMemberIdAndOrganizationId(UUID memberId, UUID organizationId, boolean includeDeactivated); @Query(""" SELECT event.* diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java index ba15d263ba..45be763b60 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringService.java @@ -11,7 +11,7 @@ public interface VolunteeringService { List listRelationships(@Nullable UUID memberId, @Nullable UUID organizationId, boolean includeDeactivated); - List listEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, boolean includeDeactivated); + List listEvents(@Nullable UUID memberId, @Nullable UUID organizationId, boolean includeDeactivated); VolunteeringOrganization create(VolunteeringOrganization organization); @@ -24,4 +24,6 @@ public interface VolunteeringService { VolunteeringRelationship update(VolunteeringRelationship relationship); VolunteeringEvent update(VolunteeringEvent event); + + void deleteEvent(UUID id); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index 40b7f3c33d..a732ab1a1e 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -7,8 +7,6 @@ import com.objectcomputing.checkins.services.role.role_permissions.RolePermissionServices; import io.micronaut.core.annotation.Nullable; import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.List; import java.util.UUID; @@ -16,7 +14,6 @@ @Singleton class VolunteeringServiceImpl implements VolunteeringService { - private static final Logger LOG = LoggerFactory.getLogger(VolunteeringServiceImpl.class); private static final String ORG_NAME_ALREADY_EXISTS_MESSAGE = "Volunteering Organization with name %s already exists"; private final MemberProfileRepository memberProfileRepository; @@ -61,11 +58,13 @@ public List listRelationships(UUID memberId, UUID orga } @Override - public List listEvents(@Nullable UUID memberId, @Nullable UUID relationshipId, boolean includeDeactivated) { - if (memberId != null) { + public List listEvents(@Nullable UUID memberId, @Nullable UUID organizationId, boolean includeDeactivated) { + if (memberId != null && organizationId != null) { + return eventRepo.findByMemberIdAndOrganizationId(memberId, organizationId, includeDeactivated); + } else if (memberId != null) { return eventRepo.findByMemberId(memberId, includeDeactivated); - } else if (relationshipId != null) { - return eventRepo.findByRelationshipId(relationshipId, includeDeactivated); + } else if (organizationId != null) { + return eventRepo.findByOrganizationId(organizationId, includeDeactivated); } else { return eventRepo.findAll(includeDeactivated); } @@ -119,7 +118,18 @@ public VolunteeringRelationship update(VolunteeringRelationship relationship) { @Override public VolunteeringEvent update(VolunteeringEvent event) { - return null; + validateEvent(event, "update"); + return eventRepo.update(event); + } + + @Override + public void deleteEvent(UUID id) { + VolunteeringEvent event = eventRepo.findById(id).orElse(null); + if (event == null) { + return; + } + validateEvent(event, "delete"); + eventRepo.deleteById(id); } private void validateRelationship(VolunteeringRelationship relationship, String action) { @@ -144,7 +154,7 @@ private void validatePermission(VolunteeringRelationship relationship, String ac private void validatePermission(VolunteeringEvent event, String action) { // Fail if the user doesn't have permission to modify the event UUID currentUserId = currentUserServices.getCurrentUser().getId(); - boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_VIEW_ALL_PULSE_RESPONSES); + boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_EVENTS); boolean ownersRelationship = relationshipRepo.findById(event.getRelationshipId()).map(r -> r.getMemberId().equals(currentUserId)).orElse(false); validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, event.getRelationshipId()); } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java index cf67e33e65..f4ce5a5a92 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java @@ -33,7 +33,6 @@ class VolunteeringRelationshipControllerTest extends TestContainersSuite impleme VolunteeringClients.Relationship relationshipClient; private final static String MEMBER_AUTH = auth(MEMBER_ROLE, MEMBER_ROLE); - private final static String ADMIN_AUTH = auth(ADMIN_ROLE, ADMIN_ROLE); static private String auth(String email, String role) { return "Basic " + Base64.getEncoder().encodeToString((email + ":" + role).getBytes(StandardCharsets.UTF_8)); From 3d01b76344b763c5a31fef972edf05514a8fe69a Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 12:12:38 +0100 Subject: [PATCH 13/20] Test event creation and deletion --- .../VolunteeringEventController.java | 2 +- .../services/fixture/VolunteeringFixture.java | 5 + .../volunteering/VolunteeringClients.java | 23 +++ .../VolunteeringEventControllerTest.java | 153 ++++++++++++++++++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java index 7a219f7669..d20d3df286 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventController.java @@ -46,7 +46,7 @@ class VolunteeringEventController { * @param includeDeactivated whether to include deactivated relationships or organizations * @return list of {@link VolunteeringEvent} */ - @Get("/{?memberId,relationshipId,includeDeactivated}") + @Get("/{?memberId,organizationId,includeDeactivated}") List findEvents(@Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated) { return volunteeringService.listEvents(memberId, organizationId, Boolean.TRUE.equals(includeDeactivated)); } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java index cfb77b2bfe..bc25b5653e 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/VolunteeringFixture.java @@ -1,5 +1,6 @@ package com.objectcomputing.checkins.services.fixture; +import com.objectcomputing.checkins.services.volunteering.VolunteeringEvent; import com.objectcomputing.checkins.services.volunteering.VolunteeringOrganization; import com.objectcomputing.checkins.services.volunteering.VolunteeringRelationship; @@ -39,4 +40,8 @@ default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, U default VolunteeringRelationship createVolunteeringRelationship(UUID memberId, UUID organizationId, LocalDate startDate, LocalDate endDate, boolean active) { return getVolunteeringRelationshipRepository().save(new VolunteeringRelationship(memberId, organizationId, startDate, endDate, active)); } + + default VolunteeringEvent createVolunteeringEvent(UUID relationshipId, LocalDate now, int i, String notes) { + return getVolunteeringEventRepository().save(new VolunteeringEvent(relationshipId, now, i, notes)); + } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java index d14a2b983a..bbf09218d5 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java @@ -4,6 +4,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpResponse; import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Delete; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; @@ -50,4 +51,26 @@ interface Relationship { @Put("/{id}") VolunteeringRelationship update(@Header String authorization, @NotNull UUID id, @Body VolunteeringRelationshipDTO relationship); } + + @Client("/services/volunteer/event") + @Requires(property = VolunteeringClients.Event.ENABLED, value = "true") + interface Event { + + String ENABLED = "enable.volunteering.event.client"; + + @Get("/") + List list(@Header String authorization); + + @Get("/{?memberId,organizationId,includeDeactivated}") + List list(@Header String authorization, @Nullable UUID memberId, @Nullable UUID organizationId, @Nullable Boolean includeDeactivated); + + @Post + HttpResponse create(@Header String authorization, @Body VolunteeringEventDTO organization); + + @Put("/{id}") + VolunteeringEvent update(@Header String authorization, @NotNull UUID id, @Body VolunteeringEventDTO organization); + + @Delete("/{id}") + HttpResponse delete(@Header String authorization, @NotNull UUID id); + } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java new file mode 100644 index 0000000000..160b32edb5 --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java @@ -0,0 +1,153 @@ +package com.objectcomputing.checkins.services.volunteering; + +import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +import com.objectcomputing.checkins.services.fixture.VolunteeringFixture; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.Base64; + +import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; +import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Property(name = VolunteeringClients.Event.ENABLED, value = "true") +class VolunteeringEventControllerTest extends TestContainersSuite implements MemberProfileFixture, RoleFixture, VolunteeringFixture { + + @Inject + VolunteeringClients.Event eventClient; + + private final static String MEMBER_AUTH = auth(MEMBER_ROLE, MEMBER_ROLE); + + static private String auth(String email, String role) { + return "Basic " + Base64.getEncoder().encodeToString((email + ":" + role).getBytes(StandardCharsets.UTF_8)); + } + + @BeforeEach + void makeRoles() { + createAndAssignRoles(); + } + + @Test + void startsEmpty() { + var list = eventClient.list(MEMBER_AUTH); + assertTrue(list.isEmpty()); + } + + @Test + void memberCanCreateEventForTheirRelationships() { + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + LocalDate now = LocalDate.now(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + + var event = new VolunteeringEventDTO(relationship.getId(), now, 10, "Notes"); + var createdEvent = eventClient.create(timAuth, event); + + assertEquals(HttpStatus.CREATED, createdEvent.getStatus()); + var createdEventBody = createdEvent.body(); + assertNotNull(createdEventBody.getId()); + assertEquals(relationship.getId(), createdEventBody.getRelationshipId()); + assertEquals(now, createdEventBody.getEventDate()); + assertEquals(10, createdEventBody.getHours()); + assertEquals("Notes", createdEventBody.getNotes()); + } + + @Test + void memberCannotCreateEventForSomeoneElseRelationships() { + MemberProfile tim = createADefaultMemberProfile(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + LocalDate now = LocalDate.now(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + + var event = new VolunteeringEventDTO(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), MEMBER_ROLE); + + var createdEvent = assertThrows(HttpClientResponseException.class, () -> eventClient.create(bobAuth, event)); + assertEquals(HttpStatus.BAD_REQUEST, createdEvent.getStatus()); + assertEquals("Member %s does not have permission to create Volunteering event for relationship %s".formatted(bob.getId(), relationship.getId()), createdEvent.getMessage()); + } + + @Test + void memberWithPermissionCanCreateEventForSomeoneElseRelationships() { + MemberProfile tim = createADefaultMemberProfile(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + LocalDate now = LocalDate.now(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), ADMIN_ROLE); + + var event = new VolunteeringEventDTO(relationship.getId(), now, 10, "Notes"); + var createdEvent = eventClient.create(bobAuth, event); + + assertEquals(HttpStatus.CREATED, createdEvent.getStatus()); + var createdEventBody = createdEvent.body(); + assertNotNull(createdEventBody.getId()); + assertEquals(relationship.getId(), createdEventBody.getRelationshipId()); + assertEquals(now, createdEventBody.getEventDate()); + assertEquals(10, createdEventBody.getHours()); + assertEquals("Notes", createdEventBody.getNotes()); + } + + @Test + void memberCanDeleteTheirOwnEvents() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + var deletedEvent = eventClient.delete(timAuth, event.getId()); + assertEquals(HttpStatus.OK, deletedEvent.getStatus()); + } + + @Test + void memberCannotDeleteOthersEvents() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), MEMBER_ROLE); + + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.delete(bobAuth, event.getId())); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Member %s does not have permission to delete Volunteering event for relationship %s".formatted(bob.getId(), relationship.getId()), e.getMessage()); + } + + @Test + void memberWithPermissionCanDeleteOthersEvents() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), ADMIN_ROLE); + + var deletedEvent = eventClient.delete(bobAuth, event.getId()); + assertEquals(HttpStatus.OK, deletedEvent.getStatus()); + } +} From 568d0818d8636ef0d5beacae5ec23aece0c53fa1 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 13:11:40 +0100 Subject: [PATCH 14/20] Fix test --- .../volunteering/VolunteeringOrganizationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java index bd629ba6ec..e6ff6df0ee 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringOrganizationController.java @@ -70,7 +70,7 @@ VolunteeringOrganization create(@Valid @Body VolunteeringOrganizationDTO organiz * @return the updated {@link VolunteeringOrganization} */ @Put("/{id}") -// @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) + @RequiredPermission(Permission.CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS) VolunteeringOrganization update(@NotNull UUID id, @Valid @Body VolunteeringOrganizationDTO organization) { return volunteeringService.update(new VolunteeringOrganization( id, From ca793af67fcec0a111f401303ff02f9be0c06df1 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 16:21:29 +0100 Subject: [PATCH 15/20] More event tests --- .../volunteering/VolunteeringEvent.java | 4 + .../volunteering/VolunteeringEventDTO.java | 5 +- .../VolunteeringEventRepository.java | 23 ++- .../volunteering/VolunteeringServiceImpl.java | 4 +- .../VolunteeringEventControllerTest.java | 190 ++++++++++++++++++ 5 files changed, 211 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java index 8a5af7e1be..d8bb12ee2e 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java @@ -14,9 +14,11 @@ import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import java.time.LocalDate; import java.util.UUID; @@ -27,6 +29,8 @@ @AllArgsConstructor @NoArgsConstructor @Introspected +@EqualsAndHashCode +@ToString @Table(name = "volunteering_event") public class VolunteeringEvent { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java index 5e70469306..43f6b923c4 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java @@ -36,14 +36,15 @@ public class VolunteeringEventDTO { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") private LocalDate eventDate; + @NotNull @Schema(description = "number of hours spent volunteering") - private int hours; + private Integer hours; @Nullable @Schema(description = "notes about the volunteering event") private String notes; - public VolunteeringEventDTO(@NotNull UUID relationshipId, @NotNull LocalDate eventDate, int hours) { + public VolunteeringEventDTO(@NotNull UUID relationshipId, @NotNull LocalDate eventDate, Integer hours) { this(relationshipId, eventDate, hours, null); } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java index c8f6d72443..1e3a1ae1cd 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java @@ -4,6 +4,7 @@ import io.micronaut.data.jdbc.annotation.JdbcRepository; import io.micronaut.data.model.query.builder.sql.Dialect; import io.micronaut.data.repository.CrudRepository; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; @@ -16,34 +17,34 @@ public interface VolunteeringEventRepository extends CrudRepository findByMemberId(UUID memberId, boolean includeDeactivated); + ORDER BY event.event_date, org.name, event.hours DESC""") + List findByMemberId(@NotNull UUID memberId, boolean includeDeactivated); @Query(""" SELECT event.* FROM volunteering_event AS event LEFT JOIN volunteering_relationship AS rel USING(relationship_id) LEFT JOIN volunteering_organization AS org USING(organization_id) - WHERE org.organization_id = :organizationId + WHERE org.organization_id::uuid = :organizationId AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) AND (org.is_active = TRUE OR :includeDeactivated = TRUE) - ORDER BY event.event_date, org.name""") - List findByOrganizationId(UUID organizationId, boolean includeDeactivated); + ORDER BY event.event_date, org.name, event.hours DESC""") + List findByOrganizationId(@NotNull UUID organizationId, boolean includeDeactivated); @Query(""" SELECT event.* FROM volunteering_event AS event LEFT JOIN volunteering_relationship AS rel USING(relationship_id) LEFT JOIN volunteering_organization AS org USING(organization_id) - WHERE rel.member_id = :memberId - AND org.organization_id = :organizationId + WHERE rel.member_id::uuid = :memberId + AND org.organization_id::uuid = :organizationId AND (rel.is_active = TRUE OR :includeDeactivated = TRUE) AND (org.is_active = TRUE OR :includeDeactivated = TRUE) - ORDER BY event.event_date, org.name""") - List findByMemberIdAndOrganizationId(UUID memberId, UUID organizationId, boolean includeDeactivated); + ORDER BY event.event_date, org.name, event.hours DESC""") + List findByMemberIdAndOrganizationId(@NotNull UUID memberId, @NotNull UUID organizationId, boolean includeDeactivated); @Query(""" SELECT event.* @@ -52,6 +53,6 @@ LEFT JOIN volunteering_relationship AS rel USING(relationship_id) LEFT JOIN volunteering_organization AS org USING(organization_id) WHERE (rel.is_active = TRUE OR :includeDeactivated = TRUE) AND (org.is_active = TRUE OR :includeDeactivated = TRUE) - ORDER BY event.event_date, org.name""") + ORDER BY event.event_date, org.name, event.hours DESC""") List findAll(boolean includeDeactivated); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index a732ab1a1e..6917266e8d 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -134,12 +134,12 @@ public void deleteEvent(UUID id) { private void validateRelationship(VolunteeringRelationship relationship, String action) { validate(memberProfileRepository.findById(relationship.getMemberId()).isEmpty(), "Member %s doesn't exist", relationship.getMemberId()); - validate(organizationRepo.findById(relationship.getOrganizationId()).isEmpty(), "Organization %s doesn't exist", relationship.getOrganizationId()); + validate(organizationRepo.findById(relationship.getOrganizationId()).isEmpty(), "Volunteering organization %s doesn't exist", relationship.getOrganizationId()); validatePermission(relationship, action); } private void validateEvent(VolunteeringEvent event, String action) { - validate(relationshipRepo.findById(event.getRelationshipId()).isEmpty(), "Relationship %s doesn't exist", event.getRelationshipId()); + validate(relationshipRepo.findById(event.getRelationshipId()).isEmpty(), "Volunteering relationship %s doesn't exist", event.getRelationshipId()); validate(event.getHours() < 0, "Hours must be non-negative"); validatePermission(event, action); } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java index 160b32edb5..d86b2f0e87 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java @@ -6,7 +6,10 @@ import com.objectcomputing.checkins.services.fixture.VolunteeringFixture; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import io.micronaut.context.annotation.Property; +import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientResponseException; import jakarta.inject.Inject; import org.junit.jupiter.api.BeforeEach; @@ -15,6 +18,8 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.Base64; +import java.util.List; +import java.util.UUID; import static com.objectcomputing.checkins.services.role.RoleType.Constants.ADMIN_ROLE; import static com.objectcomputing.checkins.services.role.RoleType.Constants.MEMBER_ROLE; @@ -29,6 +34,10 @@ class VolunteeringEventControllerTest extends TestContainersSuite implements Mem @Inject VolunteeringClients.Event eventClient; + @Inject + @Client("/services/volunteer/event") + HttpClient httpClient; + private final static String MEMBER_AUTH = auth(MEMBER_ROLE, MEMBER_ROLE); static private String auth(String email, String role) { @@ -150,4 +159,185 @@ void memberWithPermissionCanDeleteOthersEvents() { var deletedEvent = eventClient.delete(bobAuth, event.getId()); assertEquals(HttpStatus.OK, deletedEvent.getStatus()); } + + /** + * / + * ├── liftForLife + * │ ├── aliceLiftForLife + * │ │ ├── aliceLiftEvent1 + * │ │ └── aliceLiftEvent2 + * │ ├── bobLiftForLife (INACTIVE) + * │ │ └── bobLiftEvent1 + * │ └── clairLiftForLife (INACTIVE) + * │ └── clairLiftEvent1 + * ├── foodBank + * │ ├── aliceFood + * │ │ └── aliceFoodEvent1 + * │ └── clairFood + * │ └── clairFoodEvent1 + * └── closedOrg (INACTIVE) + * ├── bobClosed (INACTIVE) + * │ └── bobClosedEvent1 + * └── clairClosed + * └── clairClosedEvent1 + */ + @Test + void eventListCanBeFiltered() { + MemberProfile alice = memberWithoutBoss("alice"); + MemberProfile bob = memberWithoutBoss("bob"); + MemberProfile claire = memberWithoutBoss("clair"); + + LocalDate now = LocalDate.now(); + + var liftForLife = createVolunteeringOrganization("Lift for Life", "Educate, empower, uplift", "https://www.liftforlifeacademy.org"); + var foodBank = createVolunteeringOrganization("St. Louis Area Foodbank", "Works with over 600 partners", "https://stlfoodbank.org/find-food/"); + var closedOrg = createVolunteeringOrganization("Closed Organization", "No longer active", "https://example.com", false); + + var aliceLiftForLife = createVolunteeringRelationship(alice.getId(), liftForLife.getId(), now.minusDays(2)); + var bobLiftForLife = createVolunteeringRelationship(bob.getId(), liftForLife.getId(), now, null, false); + var claireLiftForLife = createVolunteeringRelationship(claire.getId(), liftForLife.getId(), now.minusDays(3), null, false); + var aliceFood = createVolunteeringRelationship(alice.getId(), foodBank.getId(), now.minusDays(20)); + var claireFood = createVolunteeringRelationship(claire.getId(), foodBank.getId(), now.minusDays(4), now); + var bobClosed = createVolunteeringRelationship(bob.getId(), closedOrg.getId(), now.minusDays(100), now.minusDays(50), false); + var clairClosed = createVolunteeringRelationship(claire.getId(), closedOrg.getId(), now.minusDays(1), now); + + var aliceLiftEvent1 = createVolunteeringEvent(aliceLiftForLife.getId(), now.minusDays(2), 10, "aliceLiftEvent1"); // 2 days ago + var aliceLiftEvent2 = createVolunteeringEvent(aliceLiftForLife.getId(), now, 8, "aliceLiftEvent2"); // today + var bobLiftEvent1 = createVolunteeringEvent(bobLiftForLife.getId(), now, 6, "bobLiftEvent1"); // today + var clairLiftEvent1 = createVolunteeringEvent(claireLiftForLife.getId(), now.minusDays(3), 4, "clairLiftEvent1"); // 3 days ago + var aliceFoodEvent1 = createVolunteeringEvent(aliceFood.getId(), now.minusDays(20), 2, "aliceFoodEvent1"); // 20 days ago + var clairFoodEvent1 = createVolunteeringEvent(claireFood.getId(), now, 1, "clairFoodEvent1"); // today + var bobClosedEvent1 = createVolunteeringEvent(bobClosed.getId(), now.minusDays(76), 10, "bobClosedEvent1"); // 76 days ago + var clairClosedEvent1 = createVolunteeringEvent(clairClosed.getId(), now.minusDays(1), 0, "clairClosedEvent1"); // yesterday + + // List all events, sorted by event date and then by organization name + var list = eventClient.list(MEMBER_AUTH); + assertEquals(List.of(aliceFoodEvent1, aliceLiftEvent1, aliceLiftEvent2, clairFoodEvent1), list); + + // Can filter by member + list = eventClient.list(MEMBER_AUTH, alice.getId(), null, null); + assertEquals(List.of(aliceFoodEvent1, aliceLiftEvent1, aliceLiftEvent2), list); + + // Can filter by organization + list = eventClient.list(MEMBER_AUTH, null, foodBank.getId(), null); + assertEquals(List.of(aliceFoodEvent1, clairFoodEvent1), list); + + // Can filter by organization and member + list = eventClient.list(MEMBER_AUTH, claire.getId(), foodBank.getId(), null); + assertEquals(List.of(clairFoodEvent1), list); + + // Can include deactivated + list = eventClient.list(MEMBER_AUTH, null, null, true); + assertEquals(List.of(bobClosedEvent1, aliceFoodEvent1, clairLiftEvent1, aliceLiftEvent1, clairClosedEvent1, aliceLiftEvent2, bobLiftEvent1, clairFoodEvent1), list); + + // closedOrg is inactive, so no events should be returned + list = eventClient.list(MEMBER_AUTH, null, closedOrg.getId(), false); + assertTrue(list.isEmpty()); + + // We can show events for inactive organizations + list = eventClient.list(MEMBER_AUTH, null, closedOrg.getId(), true); + assertEquals(List.of(bobClosedEvent1, clairClosedEvent1), list); + + // And we can limit to a specific member + list = eventClient.list(MEMBER_AUTH, claire.getId(), closedOrg.getId(), true); + assertEquals(List.of(clairClosedEvent1), list); + } + + @Test + void relationshipMustExist() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + UUID randomId = UUID.randomUUID(); + + VolunteeringEventDTO newEvent = new VolunteeringEventDTO(randomId, now, 10, "Notes"); + + // Creating an event with a non-existent relationship should fail + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.create(timAuth, newEvent)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Volunteering relationship %s doesn't exist".formatted(randomId), e.getMessage()); + + // Updating an event to have a non-existent relationship should fail + e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(timAuth, event.getId(), newEvent)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Volunteering relationship %s doesn't exist".formatted(randomId), e.getMessage()); + } + + @Test + void eventDateMustBeSet() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + VolunteeringEventDTO newEvent = new VolunteeringEventDTO(relationship.getId(), null, 10, "Notes"); + + // Creating an event with a null date should fail + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.create(timAuth, newEvent)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + String body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("event.eventDate: must not be null"), body + " should contain 'event.eventDate: must not be null'"); + + // Updating an event to have a null date should fail + e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(timAuth, event.getId(), newEvent)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("event.eventDate: must not be null"), body + " should contain 'event.eventDate: must not be null'"); + } + + @Test + void hoursMustBeNonNegative() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + VolunteeringEventDTO newEvent = new VolunteeringEventDTO(relationship.getId(), now, -1, "Notes"); + + // Creating an event with negative hours should fail + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.create(timAuth, newEvent)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Hours must be non-negative", e.getMessage()); + + // Updating an event to have negative hours should fail + e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(timAuth, event.getId(), newEvent)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Hours must be non-negative", e.getMessage()); + } + + @Test + void hoursAreRequired() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + String postBody = """ + { + "relationshipId": "%s", + "eventDate": "2024-06-01", + "notes": "Notes" + }""".formatted(relationship.getId()); + + var postRequest = HttpRequest.POST("/", postBody).basicAuth(tim.getWorkEmail(), MEMBER_ROLE); + var e = assertThrows(HttpClientResponseException.class, () -> httpClient.toBlocking().exchange(postRequest, VolunteeringEvent.class)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + String body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("event.hours: must not be null"), body + " should contain 'event.hours: must not be null'"); + + // Updating an event to have null hours should fail + var putRequest = HttpRequest.PUT("/" + event.getId(), postBody).basicAuth(tim.getWorkEmail(), MEMBER_ROLE); + e = assertThrows(HttpClientResponseException.class, () -> httpClient.toBlocking().exchange(putRequest, VolunteeringEvent.class)); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + body = e.getResponse().getBody(String.class).get(); + assertTrue(body.contains("event.hours: must not be null"), body + " should contain 'event.hours: must not be null'"); + } } From b9e0c4a5899e465e1a67077dfce1c050f0dff36c Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 17:06:53 +0100 Subject: [PATCH 16/20] Cleanup --- .../volunteering/VolunteeringEvent.java | 20 +++++++++++-------- .../volunteering/VolunteeringEventDTO.java | 13 ------------ .../volunteering/VolunteeringClients.java | 7 +++++-- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java index d8bb12ee2e..3f313998bc 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEvent.java @@ -14,13 +14,13 @@ import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import java.time.LocalDate; +import java.util.Objects; import java.util.UUID; @Setter @@ -29,7 +29,6 @@ @AllArgsConstructor @NoArgsConstructor @Introspected -@EqualsAndHashCode @ToString @Table(name = "volunteering_event") public class VolunteeringEvent { @@ -64,15 +63,20 @@ public class VolunteeringEvent { @Schema(description = "notes about the volunteering event") private String notes; - public VolunteeringEvent(UUID relationshipId, LocalDate eventDate, int hours) { - this(null, relationshipId, eventDate, hours, null); + public VolunteeringEvent(UUID relationshipId, LocalDate eventDate, int hours, String notes) { + this(null, relationshipId, eventDate, hours, notes); } - public VolunteeringEvent(UUID relationshipId, LocalDate eventDate) { - this(null, relationshipId, eventDate, 0, null); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VolunteeringEvent that = (VolunteeringEvent) o; + return hours == that.hours && Objects.equals(id, that.id) && Objects.equals(relationshipId, that.relationshipId) && Objects.equals(eventDate, that.eventDate) && Objects.equals(notes, that.notes); } - public VolunteeringEvent(UUID relationshipId, LocalDate eventDate, int hours, String notes) { - this(null, relationshipId, eventDate, hours, notes); + @Override + public int hashCode() { + return Objects.hash(id, relationshipId, eventDate, hours, notes); } } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java index 43f6b923c4..4d56245e01 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventDTO.java @@ -1,21 +1,12 @@ package com.objectcomputing.checkins.services.volunteering; import com.fasterxml.jackson.annotation.JsonFormat; -import com.objectcomputing.checkins.converter.LocalDateConverter; import io.micronaut.core.annotation.Introspected; -import io.micronaut.data.annotation.AutoPopulated; -import io.micronaut.data.annotation.TypeDef; -import io.micronaut.data.model.DataType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDate; @@ -43,8 +34,4 @@ public class VolunteeringEventDTO { @Nullable @Schema(description = "notes about the volunteering event") private String notes; - - public VolunteeringEventDTO(@NotNull UUID relationshipId, @NotNull LocalDate eventDate, Integer hours) { - this(relationshipId, eventDate, hours, null); - } } \ No newline at end of file diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java index bbf09218d5..e03b645f0d 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringClients.java @@ -17,13 +17,16 @@ class VolunteeringClients { + private VolunteeringClients() { + } + @Client("/services/volunteer/organization") @Requires(property = VolunteeringClients.Organization.ENABLED, value = "true") interface Organization { String ENABLED = "enable.volunteering.organization.client"; - @Get("/") + @Get List list(@Header String authorization); @Get("/{?includeDeactivated}") @@ -58,7 +61,7 @@ interface Event { String ENABLED = "enable.volunteering.event.client"; - @Get("/") + @Get List list(@Header String authorization); @Get("/{?memberId,organizationId,includeDeactivated}") From c44f6eead98e95b65c1331b775b818faa5557de0 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Tue, 11 Jun 2024 17:38:24 +0100 Subject: [PATCH 17/20] Test update and fix permissions check --- .../volunteering/VolunteeringServiceImpl.java | 17 ++++- .../VolunteeringEventControllerTest.java | 63 +++++++++++++++++++ ...olunteeringRelationshipControllerTest.java | 19 ++++++ 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index 6917266e8d..d308536daa 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -148,15 +148,26 @@ private void validatePermission(VolunteeringRelationship relationship, String ac // Fail if the user doesn't have permission to modify the relationship UUID currentUserId = currentUserServices.getCurrentUser().getId(); boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS); - validate(!hasPermission && !relationship.getMemberId().equals(currentUserId), "Member %s does not have permission to %s Volunteering relationship for member %s", currentUserId, action, relationship.getMemberId()); + // Be sure to go and check the member id from the relationship in the DB, not the one passed in here + UUID memberId = relationship.getId() == null + ? relationship.getMemberId() + : relationshipRepo.findById(relationship.getId()).map(VolunteeringRelationship::getMemberId).orElseThrow(() -> new BadArgException("Unknown member %s".formatted(relationship.getMemberId()))); + boolean ownersRelationship = memberId.equals(currentUserId); + validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering relationship for member %s", currentUserId, action, memberId); } private void validatePermission(VolunteeringEvent event, String action) { // Fail if the user doesn't have permission to modify the event UUID currentUserId = currentUserServices.getCurrentUser().getId(); boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_EVENTS); - boolean ownersRelationship = relationshipRepo.findById(event.getRelationshipId()).map(r -> r.getMemberId().equals(currentUserId)).orElse(false); - validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, event.getRelationshipId()); + // Be sure to go and check the event in the DB, not the one passed in here + UUID relationshipId = event.getId() == null + ? event.getRelationshipId() + : eventRepo.findById(event.getId()).map(VolunteeringEvent::getRelationshipId).orElseThrow(() -> new BadArgException("Unknown relationship %s".formatted(event.getRelationshipId()))); + boolean ownersRelationship = relationshipRepo.findById(relationshipId) + .map(r -> r.getMemberId().equals(currentUserId)) + .orElse(false); + validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, relationshipId); } private void validate(boolean isError, String message, Object... args) { diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java index d86b2f0e87..00b5c54e61 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java @@ -116,6 +116,69 @@ void memberWithPermissionCanCreateEventForSomeoneElseRelationships() { assertEquals("Notes", createdEventBody.getNotes()); } + @Test + void memberCanUpdateTheirOwnEvents() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + var updated = eventClient.update(timAuth, event.getId(), new VolunteeringEventDTO(relationship.getId(), now, 5, "New notes")); + assertEquals(event.getId(), updated.getId()); + assertEquals("New notes", updated.getNotes()); + } + + @Test + void memberCannotUpdateOthersEvents() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), MEMBER_ROLE); + + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(bobAuth, event.getId(), new VolunteeringEventDTO(relationship.getId(), now, 5, "New notes"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering event for relationship %s".formatted(bob.getId(), relationship.getId()), e.getMessage()); + } + + @Test + void memberCannotHackUpdateOthersEventsWithTheirOwnRelationship() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), MEMBER_ROLE); + VolunteeringRelationship bobsRelationship = createVolunteeringRelationship(bob.getId(), organization.getId(), now); + + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(bobAuth, event.getId(), new VolunteeringEventDTO(bobsRelationship.getId(), now, 5, "New notes"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering event for relationship %s".formatted(bob.getId(), relationship.getId()), e.getMessage()); + } + + @Test + void memberCanUpdateOthersEventsWithProperPermission() { + LocalDate now = LocalDate.now(); + MemberProfile tim = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), ADMIN_ROLE); + + var updated = eventClient.update(bobAuth, event.getId(), new VolunteeringEventDTO(relationship.getId(), now, 5, "New notes")); + assertEquals(event.getId(), updated.getId()); + assertEquals("New notes", updated.getNotes()); + } + @Test void memberCanDeleteTheirOwnEvents() { LocalDate now = LocalDate.now(); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java index f4ce5a5a92..c9ebc97296 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java @@ -129,6 +129,25 @@ void cannotUpdateOtherMembersRelationshipsWithoutPermission() { assertEquals("Member %s does not have permission to update Volunteering relationship for member %s".formatted(memberProfile.getId(), sarah.getId()), update.getMessage()); } + @Test + void cannotHackUpdateOtherMembersRelationshipsWithoutPermission() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + + LocalDate startDate = LocalDate.now(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(sarah.getId(), organization.getId(), startDate); + + VolunteeringRelationshipDTO updateDto = new VolunteeringRelationshipDTO(memberProfile.getId(), relationship.getOrganizationId(), startDate.plusDays(1), LocalDate.now()); + + var update = assertThrows(HttpClientResponseException.class, () -> relationshipClient.update(memberAuth, relationship.getId(), updateDto)); + assertEquals(HttpStatus.BAD_REQUEST, update.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering relationship for member %s".formatted(memberProfile.getId(), sarah.getId()), update.getMessage()); + } + @Test void canUpdateOtherMembersRelationshipsWithPermission() { MemberProfile memberProfile = createADefaultMemberProfile(); From b35a318f90d883c760033f9c1617ef3e437514ae Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 12 Jun 2024 10:03:16 +0100 Subject: [PATCH 18/20] Check member in db too to prevent swapping of relationships and events --- .../VolunteeringEventRepository.java | 16 +++---- .../VolunteeringRelationshipRepository.java | 19 ++++++-- .../volunteering/VolunteeringServiceImpl.java | 43 +++++++++++++------ .../VolunteeringEventControllerTest.java | 40 +++++++++++++++++ ...olunteeringRelationshipControllerTest.java | 19 ++++++++ 5 files changed, 111 insertions(+), 26 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java index 1e3a1ae1cd..40967551aa 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventRepository.java @@ -15,8 +15,8 @@ public interface VolunteeringEventRepository extends CrudRepository findAll(boolean includeDeactivated); + + @Query(""" + SELECT rel.* + FROM volunteering_event AS event + JOIN volunteering_relationship AS rel USING(relationship_id) + WHERE event.event_id::uuid = :eventId""") + Optional getRelationshipForEvent(@Nullable UUID eventId); + + Optional findById(@Nullable UUID eventId); } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index d308536daa..281442c53b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -9,6 +9,7 @@ import jakarta.inject.Singleton; import java.util.List; +import java.util.Optional; import java.util.UUID; @Singleton @@ -148,26 +149,40 @@ private void validatePermission(VolunteeringRelationship relationship, String ac // Fail if the user doesn't have permission to modify the relationship UUID currentUserId = currentUserServices.getCurrentUser().getId(); boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS); - // Be sure to go and check the member id from the relationship in the DB, not the one passed in here - UUID memberId = relationship.getId() == null - ? relationship.getMemberId() - : relationshipRepo.findById(relationship.getId()).map(VolunteeringRelationship::getMemberId).orElseThrow(() -> new BadArgException("Unknown member %s".formatted(relationship.getMemberId()))); - boolean ownersRelationship = memberId.equals(currentUserId); - validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering relationship for member %s", currentUserId, action, memberId); + if (hasPermission) { + return; + } + + // Check the member in the request + if (!relationship.getMemberId().equals(currentUserId)) { + throw new BadArgException("Member %s does not have permission to %s Volunteering relationship for member %s".formatted(currentUserId, action, relationship.getMemberId())); + } + + // And check the owner in the database + Optional fromDb = relationshipRepo.findById(relationship.getId()); + if (fromDb.map(r -> !r.getMemberId().equals(currentUserId)).orElse(false)) { + throw new BadArgException("Member %s does not have permission to %s Volunteering relationship for member %s".formatted(currentUserId, action, fromDb.map(VolunteeringRelationship::getMemberId).orElse(null))); + } } private void validatePermission(VolunteeringEvent event, String action) { // Fail if the user doesn't have permission to modify the event UUID currentUserId = currentUserServices.getCurrentUser().getId(); boolean hasPermission = rolePermissionServices.findUserPermissions(currentUserId).contains(Permission.CAN_ADMINISTER_VOLUNTEERING_EVENTS); - // Be sure to go and check the event in the DB, not the one passed in here - UUID relationshipId = event.getId() == null - ? event.getRelationshipId() - : eventRepo.findById(event.getId()).map(VolunteeringEvent::getRelationshipId).orElseThrow(() -> new BadArgException("Unknown relationship %s".formatted(event.getRelationshipId()))); - boolean ownersRelationship = relationshipRepo.findById(relationshipId) - .map(r -> r.getMemberId().equals(currentUserId)) - .orElse(false); - validate(!hasPermission && !ownersRelationship, "Member %s does not have permission to %s Volunteering event for relationship %s", currentUserId, action, relationshipId); + if (hasPermission) { + return; + } + + // Check the owner of the relationship in the request + if (!relationshipRepo.findById(event.getRelationshipId()).map(r -> r.getMemberId().equals(currentUserId)).orElse(false)) { + throw new BadArgException("Member %s does not have permission to %s Volunteering event for relationship %s".formatted(currentUserId, action, event.getRelationshipId())); + } + + // And check the owner in the database + Optional relationshipMemberForEvent = relationshipRepo.getRelationshipForEvent(event.getId()); + if (relationshipMemberForEvent.map(i -> !i.getMemberId().equals(currentUserId)).orElse(false)) { + throw new BadArgException("Member %s does not have permission to %s Volunteering event for relationship %s".formatted(currentUserId, action, relationshipMemberForEvent.map(VolunteeringRelationship::getId).orElse(null))); + } } private void validate(boolean isError, String message, Object... args) { diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java index 00b5c54e61..804edaf2bf 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringEventControllerTest.java @@ -121,6 +121,7 @@ void memberCanUpdateTheirOwnEvents() { LocalDate now = LocalDate.now(); MemberProfile tim = createADefaultMemberProfile(); String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); @@ -134,6 +135,7 @@ void memberCanUpdateTheirOwnEvents() { void memberCannotUpdateOthersEvents() { LocalDate now = LocalDate.now(); MemberProfile tim = createADefaultMemberProfile(); + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); @@ -146,6 +148,44 @@ void memberCannotUpdateOthersEvents() { assertEquals("Member %s does not have permission to update Volunteering event for relationship %s".formatted(bob.getId(), relationship.getId()), e.getMessage()); } + @Test + void memberCannotUpdateTheirEventToSomeoneElse() { + LocalDate now = LocalDate.now(); + + MemberProfile tim = createADefaultMemberProfile(); + String timAuth = auth(tim.getWorkEmail(), MEMBER_ROLE); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + VolunteeringRelationship bobRelationship = createVolunteeringRelationship(bob.getId(), organization.getId(), now); + + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(timAuth, event.getId(), new VolunteeringEventDTO(bobRelationship.getId(), now, 5, "New notes"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering event for relationship %s".formatted(tim.getId(), bobRelationship.getId()), e.getMessage()); + } + + @Test + void memberCannotUpdateSomeoneElseEventToTheirs() { + LocalDate now = LocalDate.now(); + + MemberProfile tim = createADefaultMemberProfile(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(tim.getId(), organization.getId(), now); + VolunteeringEvent event = createVolunteeringEvent(relationship.getId(), now, 10, "Notes"); + + MemberProfile bob = memberWithoutBoss("bob"); + String bobAuth = auth(bob.getWorkEmail(), MEMBER_ROLE); + VolunteeringRelationship bobRelationship = createVolunteeringRelationship(bob.getId(), organization.getId(), now); + + var e = assertThrows(HttpClientResponseException.class, () -> eventClient.update(bobAuth, event.getId(), new VolunteeringEventDTO(bobRelationship.getId(), now, 5, "New notes"))); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering event for relationship %s".formatted(bob.getId(), relationship.getId()), e.getMessage()); + } + @Test void memberCannotHackUpdateOthersEventsWithTheirOwnRelationship() { LocalDate now = LocalDate.now(); diff --git a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java index c9ebc97296..feb709e2d7 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/volunteering/VolunteeringRelationshipControllerTest.java @@ -148,6 +148,25 @@ void cannotHackUpdateOtherMembersRelationshipsWithoutPermission() { assertEquals("Member %s does not have permission to update Volunteering relationship for member %s".formatted(memberProfile.getId(), sarah.getId()), update.getMessage()); } + @Test + void cannotHackUpdateMyRelationshipsToOtherPeopleWithoutPermission() { + MemberProfile memberProfile = createADefaultMemberProfile(); + String memberAuth = auth(memberProfile.getWorkEmail(), MEMBER_ROLE); + + MemberProfile sarah = memberWithoutBoss("sarah"); + + LocalDate startDate = LocalDate.now(); + + VolunteeringOrganization organization = createDefaultVolunteeringOrganization(); + VolunteeringRelationship relationship = createVolunteeringRelationship(memberProfile.getId(), organization.getId(), startDate); + + VolunteeringRelationshipDTO updateDto = new VolunteeringRelationshipDTO(sarah.getId(), relationship.getOrganizationId(), startDate.plusDays(1), LocalDate.now()); + + var update = assertThrows(HttpClientResponseException.class, () -> relationshipClient.update(memberAuth, relationship.getId(), updateDto)); + assertEquals(HttpStatus.BAD_REQUEST, update.getStatus()); + assertEquals("Member %s does not have permission to update Volunteering relationship for member %s".formatted(memberProfile.getId(), sarah.getId()), update.getMessage()); + } + @Test void canUpdateOtherMembersRelationshipsWithPermission() { MemberProfile memberProfile = createADefaultMemberProfile(); From 88f5c90ae1b2d9b70fdd11c2e399d9dd33406444 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 12 Jun 2024 10:06:39 +0100 Subject: [PATCH 19/20] Delete volunteering in dev data migration --- server/src/main/resources/db/dev/R__Load_testing_data.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/resources/db/dev/R__Load_testing_data.sql b/server/src/main/resources/db/dev/R__Load_testing_data.sql index 74d0227f91..4b9c97ff70 100644 --- a/server/src/main/resources/db/dev/R__Load_testing_data.sql +++ b/server/src/main/resources/db/dev/R__Load_testing_data.sql @@ -7,6 +7,9 @@ delete from checkins; delete from guild_member_history; delete from guild_member; delete from guild; +delete from volunteering_event; +delete from volunteering_relationship; +delete from volunteering_organization; delete from member_skills; delete from pulse_response; delete from questions; From 680d54b73f55a71b1486409e966bdc40944cd020 Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Wed, 12 Jun 2024 16:21:30 +0100 Subject: [PATCH 20/20] Fix comments --- .../services/volunteering/VolunteeringServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java index 281442c53b..184e1ba0c2 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/volunteering/VolunteeringServiceImpl.java @@ -76,7 +76,7 @@ public VolunteeringOrganization create(VolunteeringOrganization organization) { if (organization.getId() != null) { return update(organization); } - // Fail if a certification with the same name already exists + // Fail if an organization with the same name already exists validate(organizationRepo.getByName(organization.getName()).isPresent(), ORG_NAME_ALREADY_EXISTS_MESSAGE, organization.getName()); @@ -103,7 +103,7 @@ public VolunteeringEvent create(VolunteeringEvent event) { @Override public VolunteeringOrganization update(VolunteeringOrganization organization) { - // Fail if a certification with the same name already exists (but it's not this one) + // Fail if an organization with the same name already exists (but it's not this one) validate(organizationRepo.getByName(organization.getName()) .map(c -> !c.getId().equals(organization.getId())).orElse(false), ORG_NAME_ALREADY_EXISTS_MESSAGE,