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 1001b777b1..cc25c3fe9c 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 @@ -35,7 +35,8 @@ public enum Permission { CAN_DELETE_CHECKIN_DOCUMENT("Delete check-ins document", "Check-ins"), CAN_VIEW_ALL_CHECKINS("View all check-ins", "Check-ins"), CAN_UPDATE_ALL_CHECKINS("Update all check-ins, including completed check-ins", "Check-ins"), - CAN_EDIT_SKILL_CATEGORIES("Edit skill categories", "Skill Categories"); + CAN_EDIT_SKILL_CATEGORIES("Edit skill categories", "Skill Categories"), + CAN_CREATE_REVIEW_ASSIGNMENTS("Create review assignments", "Reviews"); private final String description; private final String category; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignment.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignment.java new file mode 100644 index 0000000000..033adf3eec --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignment.java @@ -0,0 +1,91 @@ +package com.objectcomputing.checkins.services.reviews; + +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 lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; +import java.util.Objects; +import java.util.UUID; + +@Entity +@Setter +@Getter +@Introspected +@NoArgsConstructor +@Table(name = "review_assignments") +public class ReviewAssignment { + + @Id + @Column(name = "id") + @AutoPopulated + @TypeDef(type = DataType.STRING) + @Schema(description = "The id of the review assignment", required = true) + private UUID id; + + public ReviewAssignment(UUID revieweeId, UUID reviewerId, UUID reviewPeriodId, Boolean approved) { + this.revieweeId = revieweeId; + this.reviewerId = reviewerId; + this.reviewPeriodId = reviewPeriodId; + this.approved = approved; + } + + @NotBlank + @Column(name = "reviewee_id") + @Schema(required = true, description = "The ID of the employee being reviewed") + private UUID revieweeId; + + @NotBlank + @Column(name = "reviewer_id") + @Schema(required = true, description = "The ID of the employee conducting the review") + private UUID reviewerId; + + @NotBlank + @Column(name = "review_period_id") + @Schema(required = true, description = "The ID of the review period that the assignment is related to") + private UUID reviewPeriodId; + + @Nullable + @Column(name = "approved") + @Schema(description = "The status of the review assignment") + private Boolean approved; + + @Override + public int hashCode() { + return Objects.hash(id, revieweeId, reviewerId, reviewPeriodId, approved); + } + + @Override + public String toString() { + return "ReviewAssignment{" + + "id=" + id + + ", revieweeId=" + revieweeId + + ", reviewerId=" + reviewerId + + ", reviewPeriodId=" + reviewPeriodId + + ", approved=" + approved + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ReviewAssignment that = (ReviewAssignment) o; + return Objects.equals(id, that.id) && + Objects.equals(revieweeId, that.revieweeId) && + Objects.equals(reviewerId, that.reviewerId) && + Objects.equals(reviewPeriodId, that.reviewPeriodId) && + Objects.equals(approved, that.approved); + } + +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentController.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentController.java new file mode 100644 index 0000000000..6dd17ecc41 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentController.java @@ -0,0 +1,82 @@ +package com.objectcomputing.checkins.services.reviews; + +import com.objectcomputing.checkins.exceptions.NotFoundException; +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.permissions.RequiredPermission; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.*; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.netty.channel.EventLoopGroup; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Named; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.net.URI; +import java.util.UUID; +import java.util.concurrent.ExecutorService; + +@Controller("/services/review-assignments") +@Secured(SecurityRule.IS_AUTHENTICATED) +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "reviews") +public class ReviewAssignmentController { + + private ReviewAssignmentServices reviewAssignmentServices; + private final EventLoopGroup eventLoopGroup; + private final ExecutorService ioExecutorService; + + public ReviewAssignmentController(ReviewAssignmentServices reviewAssignmentServices, EventLoopGroup eventLoopGroup, @Named(TaskExecutors.IO) ExecutorService ioExecutorService) { + this.reviewAssignmentServices = reviewAssignmentServices; + this.eventLoopGroup = eventLoopGroup; + this.ioExecutorService = ioExecutorService; + } + + /** + * Create and save a new {@link ReviewAssignment}. + * + * @param assignment a {@link ReviewAssignmentDTO} representing the desired review assignment + * @return a streamable response containing the stored {@link ReviewAssignment} + */ + @Post + @RequiredPermission(Permission.CAN_CREATE_REVIEW_ASSIGNMENTS) + public Mono> createReviewAssignment(@Body @Valid ReviewAssignmentDTO assignment, HttpRequest request) { + + return Mono.fromCallable(() -> reviewAssignmentServices.save(assignment.convertToEntity())) + .publishOn(Schedulers.fromExecutor(eventLoopGroup)) + .map(reviewAssignment -> (HttpResponse) HttpResponse.created(reviewAssignment) + .headers(headers -> headers.location( + URI.create(String.format("%s/%s", request.getPath(), reviewAssignment.getId()))))) + .subscribeOn(Schedulers.fromExecutor(ioExecutorService)); + + } + + /** + * Retrieve a {@link ReviewAssignment} given its id. + * + * @param id {@link UUID} of the review assignment + * @return a streamable response containing the found {@link ReviewAssignment} with the given ID + */ + + @Get("/{id}") + public Mono> getById(@NotNull UUID id) { + + return Mono.fromCallable(() -> { + ReviewAssignment result = reviewAssignmentServices.findById(id); + if (result == null) { + throw new NotFoundException("No review assignment for UUID"); + } + return result; + }).publishOn(Schedulers.fromExecutor(eventLoopGroup)).map(reviewAssignment -> { + return (HttpResponse) HttpResponse.ok(reviewAssignment); + }).subscribeOn(Schedulers.fromExecutor(ioExecutorService)); + } + +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentDTO.java new file mode 100644 index 0000000000..460ad96ece --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentDTO.java @@ -0,0 +1,39 @@ +package com.objectcomputing.checkins.services.reviews; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Introspected +public class ReviewAssignmentDTO { + + @NotBlank + @Schema(required = true, description = "The ID of the employee being reviewed") + private UUID revieweeId; + + @NotBlank + @Schema(required = true, description = "The ID of the employee conducting the review") + private UUID reviewerId; + + @NotBlank + @Schema(required = true, description = "The ID of the review period that the assignment is related to") + private UUID reviewPeriodId; + + @Nullable + @Schema(description = "The status of the review assignment") + private Boolean approved = false; + + public ReviewAssignment convertToEntity(){ + return new ReviewAssignment(this.revieweeId, this.reviewerId, this.reviewPeriodId, this.approved); + } + +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentRepository.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentRepository.java new file mode 100644 index 0000000000..46979a4eed --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentRepository.java @@ -0,0 +1,11 @@ +package com.objectcomputing.checkins.services.reviews; + +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 ReviewAssignmentRepository extends CrudRepository { +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentServices.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentServices.java new file mode 100644 index 0000000000..3a4283dfe3 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentServices.java @@ -0,0 +1,9 @@ +package com.objectcomputing.checkins.services.reviews; + +import java.util.UUID; + +public interface ReviewAssignmentServices { + ReviewAssignment save(ReviewAssignment reviewAssignment); + ReviewAssignment findById(UUID id); + +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentServicesImpl.java new file mode 100644 index 0000000000..49d11fad35 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentServicesImpl.java @@ -0,0 +1,42 @@ +package com.objectcomputing.checkins.services.reviews; + +import com.objectcomputing.checkins.exceptions.AlreadyExistsException; +import com.objectcomputing.checkins.exceptions.BadArgException; +import jakarta.inject.Singleton; + +import javax.validation.constraints.NotNull; +import java.util.UUID; + +@Singleton +public class ReviewAssignmentServicesImpl implements ReviewAssignmentServices { + + ReviewAssignmentRepository reviewAssignmentRepository; + + public ReviewAssignmentServicesImpl(ReviewAssignmentRepository reviewAssignmentRepository) { + this.reviewAssignmentRepository = reviewAssignmentRepository; + } + + @Override + public ReviewAssignment save(ReviewAssignment reviewAssignment) { + ReviewAssignment newAssignment = null; + if (reviewAssignment != null) { + + if (reviewAssignment.getId() != null) { + throw new BadArgException(String.format("Found unexpected id %s for review assignment. New entities must not contain an id.", + reviewAssignment.getId())); + } + + //The service creates a new review assignment with an initial approved status of false. + reviewAssignment.setApproved(false); + + newAssignment = reviewAssignmentRepository.save(reviewAssignment); + } + + return newAssignment; + } + + @Override + public ReviewAssignment findById(@NotNull UUID id) { + return reviewAssignmentRepository.findById(id).orElse(null); + } +} diff --git a/server/src/main/resources/db/common/V101__add_review_assignments.sql b/server/src/main/resources/db/common/V101__add_review_assignments.sql new file mode 100644 index 0000000000..e227e0078a --- /dev/null +++ b/server/src/main/resources/db/common/V101__add_review_assignments.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS review_assignments; + +CREATE TABLE review_assignments ( + id varchar PRIMARY KEY, + reviewee_id varchar, + reviewer_id varchar, + review_period_id varchar, + approved boolean +); 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 d0795370ed..f02e1739a5 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 @@ -66,7 +66,8 @@ public interface PermissionFixture extends RolePermissionFixture { Permission.CAN_UPDATE_CHECKIN_DOCUMENT, Permission.CAN_DELETE_CHECKIN_DOCUMENT, Permission.CAN_VIEW_ALL_CHECKINS, - Permission.CAN_UPDATE_ALL_CHECKINS + Permission.CAN_UPDATE_ALL_CHECKINS, + Permission.CAN_CREATE_REVIEW_ASSIGNMENTS ); 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 a0e1472e2e..cad610022d 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 @@ -21,6 +21,7 @@ import com.objectcomputing.checkins.services.pulseresponse.PulseResponseRepository; import com.objectcomputing.checkins.services.question_category.QuestionCategoryRepository; import com.objectcomputing.checkins.services.questions.QuestionRepository; +import com.objectcomputing.checkins.services.reviews.ReviewAssignmentRepository; import com.objectcomputing.checkins.services.reviews.ReviewPeriodRepository; import com.objectcomputing.checkins.services.role.RoleRepository; import com.objectcomputing.checkins.services.role.member_roles.MemberRoleRepository; @@ -177,6 +178,10 @@ default ReviewPeriodRepository getReviewPeriodRepository() { return getEmbeddedServer().getApplicationContext().getBean(ReviewPeriodRepository.class); } + default ReviewAssignmentRepository getReviewAssignmentRepository() { + return getEmbeddedServer().getApplicationContext().getBean(ReviewAssignmentRepository.class); + } + default SkillCategoryRepository getSkillCategoryRepository() { return getEmbeddedServer().getApplicationContext().getBean(SkillCategoryRepository.class); } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/ReviewAssignmentFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/ReviewAssignmentFixture.java new file mode 100644 index 0000000000..49c1241e7d --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/ReviewAssignmentFixture.java @@ -0,0 +1,20 @@ +package com.objectcomputing.checkins.services.fixture; + +import com.objectcomputing.checkins.services.reviews.ReviewAssignment; +import com.objectcomputing.checkins.services.reviews.ReviewPeriod; +import com.objectcomputing.checkins.services.reviews.ReviewStatus; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +public interface ReviewAssignmentFixture extends RepositoryFixture { + + + default ReviewAssignment createADefaultReviewAssignment() { + + return getReviewAssignmentRepository().save(new ReviewAssignment(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), false)); + + } + +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentControllerTest.java new file mode 100644 index 0000000000..c635889e2b --- /dev/null +++ b/server/src/test/java/com/objectcomputing/checkins/services/reviews/ReviewAssignmentControllerTest.java @@ -0,0 +1,83 @@ +package com.objectcomputing.checkins.services.reviews; + +import com.objectcomputing.checkins.services.TestContainersSuite; +import com.objectcomputing.checkins.services.fixture.MemberProfileFixture; +import com.objectcomputing.checkins.services.fixture.ReviewAssignmentFixture; +import com.objectcomputing.checkins.services.fixture.ReviewPeriodFixture; +import com.objectcomputing.checkins.services.fixture.RoleFixture; +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.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.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Set; +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; +import static org.junit.jupiter.api.Assertions.*; + +public class ReviewAssignmentControllerTest extends TestContainersSuite implements ReviewAssignmentFixture, ReviewPeriodFixture, MemberProfileFixture, RoleFixture { + + @Inject + @Client("/services/review-assignments") + private HttpClient client; + + private String encodeValue(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + @BeforeEach + void createRolesAndPermissions() { + createAndAssignRoles(); + } + + + @Test + public void testPOSTCreateAReviewAssignment() { + ReviewAssignmentDTO reviewAssignmentDTO = new ReviewAssignmentDTO(); + reviewAssignmentDTO.setRevieweeId(UUID.randomUUID()); + reviewAssignmentDTO.setReviewerId(UUID.randomUUID()); + reviewAssignmentDTO.setReviewPeriodId(UUID.randomUUID()); + reviewAssignmentDTO.setApproved(false); + + final HttpRequest request = HttpRequest. + POST("/", reviewAssignmentDTO).basicAuth(ADMIN_ROLE, ADMIN_ROLE); + final HttpResponse response = client.toBlocking().exchange(request, ReviewAssignment.class); + + assertNotNull(response); + var body = response.body(); + assertNotNull(body); + assertEquals(HttpStatus.CREATED, response.getStatus()); + assertEquals(reviewAssignmentDTO.getRevieweeId(), body.getRevieweeId()); + assertEquals(reviewAssignmentDTO.getReviewerId(), body.getReviewerId()); + assertEquals(reviewAssignmentDTO.getReviewPeriodId(), body.getReviewPeriodId()); + assertEquals(false, body.getApproved()); + assertEquals(String.format("%s/%s", request.getPath(), body.getId()), response.getHeaders().get("location")); + } + + @Test + public void testGETGetByIdHappyPath() { + ReviewAssignment reviewAssignment = createADefaultReviewAssignment(); + + final HttpRequest request = HttpRequest. + GET(String.format("/%s", reviewAssignment.getId())).basicAuth(MEMBER_ROLE, MEMBER_ROLE); + + final HttpResponse response = client.toBlocking().exchange(request, ReviewAssignment.class); + + assertEquals(reviewAssignment, response.body()); + assertEquals(HttpStatus.OK, response.getStatus()); + } + +} \ No newline at end of file