diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/ResponseEntityWorkflowCommentView.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/ResponseEntityWorkflowCommentView.java new file mode 100644 index 000000000000..16a1687118d3 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/ResponseEntityWorkflowCommentView.java @@ -0,0 +1,13 @@ +package com.dotcms.rest.api.v1.workflow; + +import com.dotcms.rest.ResponseEntityView; + +/** + * This class is used to return a WorkflowTimelineItemView object as a response entity. + * @author jsanca + */ +public class ResponseEntityWorkflowCommentView extends ResponseEntityView { + public ResponseEntityWorkflowCommentView(final WorkflowTimelineItemView entity) { + super(entity); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowCommentForm.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowCommentForm.java new file mode 100644 index 000000000000..49304753739c --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowCommentForm.java @@ -0,0 +1,22 @@ +package com.dotcms.rest.api.v1.workflow; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * This class is used to represent a WorkflowCommentForm object. + * @author jsanca + */ +public class WorkflowCommentForm { + + private String comment; + + @JsonCreator + public WorkflowCommentForm(@JsonProperty("comment") final String comment) { + this.comment = comment; + } + + public String getComment() { + return comment; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java index 8921c387f416..3280af829666 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java @@ -72,6 +72,7 @@ import com.dotmarketing.portlets.workflows.model.SystemActionWorkflowActionMapping; import com.dotmarketing.portlets.workflows.model.WorkflowAction; import com.dotmarketing.portlets.workflows.model.WorkflowActionClass; +import com.dotmarketing.portlets.workflows.model.WorkflowComment; import com.dotmarketing.portlets.workflows.model.WorkflowScheme; import com.dotmarketing.portlets.workflows.model.WorkflowStep; import com.dotmarketing.portlets.workflows.model.WorkflowTask; @@ -139,6 +140,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -2641,6 +2643,7 @@ public final Response fireActionByNameMultipart(@Context final HttpServletReques return ResponseUtil.mapExceptionResponse(e); } } + /** * Fires a workflow action by name, if the contentlet exists could use inode or identifier and optional language. * @param request {@link HttpServletRequest} @@ -3702,7 +3705,6 @@ private void mergeContentletsByDefaultAction(final List cont new DotConcurrentFactory.SubmitterConfigBuilder().poolSize(2).maxPoolSize(5).queueCapacity(CONTENTLETS_LIMIT).build()); final CompletionService> completionService = new ExecutorCompletionService<>(dotSubmitter); final List>> futures = new ArrayList<>(); - // todo: add the mock request final HttpServletRequest statelessRequest = RequestUtil.INSTANCE.createStatelessRequest(request); @@ -5677,7 +5679,6 @@ public final ResponseEntityWorkflowHistoryCommentsView getWorkflowTasksHistoryCo this.contentletAPI.findContentletByIdentifierOrFallback (contentletIdentifier, mode.showLive, languageId, initDataObject.getUser(), mode.respectAnonPerms); - if (currentContentlet.isPresent()) { final WorkflowTask currentWorkflowTask = this.workflowAPI.findTaskByContentlet(currentContentlet.get()); @@ -5698,4 +5699,85 @@ private WorkflowTimelineItemView toWorkflowTimelineItemView(final WorkflowTimeli wfTimeLine.commentDescription(), wfTimeLine.taskId(), wfTimeLine.type()); } + /** + * Creates a new workflow comment + * + * @param request HttpServletRequest + * @param workflowSchemeForm WorkflowSchemeForm + * @return Response + */ + @POST + @Path("/{contentletId}/comments") + @JSONP + @NoCache + @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes({MediaType.APPLICATION_JSON}) + @Operation(operationId = "postSaveScheme", summary = "Create a workflow comment", + description = "Create a [workflow comment].\n\n " + + "Returns created workflow comment on success.", + tags = {"Workflow"}, + responses = { + @ApiResponse(responseCode = "200", description = "Copied workflow comment successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityWorkflowCommentView.class) + ) + ), + @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` + @ApiResponse(responseCode = "401", description = "Invalid User"), // not logged in + @ApiResponse(responseCode = "403", description = "Forbidden"), // no permission + @ApiResponse(responseCode = "500", description = "Internal Server Error") + } + ) + public final ResponseEntityWorkflowCommentView saveComment(@Context final HttpServletRequest request, + @Context final HttpServletResponse response, + @PathParam("contentletId") @Parameter( + required = true, + description = "Identifier of contentlet to add comment.", + schema = @Schema(type = "string") + ) final String contentletId, + @DefaultValue("-1") @QueryParam("language") @Parameter( + description = "Language version of target content.", + schema = @Schema(type = "string")) final String language, + @RequestBody( + description = "The request body consists of the following three properties:\n\n" + + "| Property | Type | Description |\n" + + "|-|-|-|\n" + + "| `comment` | String | The workflow comment. |\n", + content = @Content( + schema = @Schema(implementation = WorkflowCommentForm.class) + ) + ) final WorkflowCommentForm workflowCommentForm) throws DotDataException, DotSecurityException { + + final InitDataObject initDataObject = new WebResource.InitBuilder(webResource) + .requestAndResponse(request, response) + .rejectWhenNoUser(true) + .requiredBackendUser(true).requiredFrontendUser(false).init(); + + DotPreconditions.notNull(workflowCommentForm,"Expected Request body was empty."); + Logger.debug(this, ()->"Saving a workflow comment for the contentletId: " + contentletId); + + final User user = initDataObject.getUser(); + final long languageId = LanguageUtil.getLanguageId(language); + final PageMode mode = PageMode.get(request); + + final Optional currentContentlet = languageId <= 0? + this.workflowHelper.getContentletByIdentifier(contentletId, mode, initDataObject.getUser(), + ()->WebAPILocator.getLanguageWebAPI().getLanguage(request).getId()): + this.contentletAPI.findContentletByIdentifierOrFallback + (contentletId, mode.showLive, languageId, initDataObject.getUser(), mode.respectAnonPerms); + if (currentContentlet.isPresent()) { + + final WorkflowTask task = this.workflowAPI.findTaskByContentlet(currentContentlet.get()); + final WorkflowComment taskComment = new WorkflowComment(); + taskComment.setComment(workflowCommentForm.getComment()); + taskComment.setCreationDate(new Date()); + taskComment.setPostedBy(user.getUserId()); + taskComment.setWorkflowtaskId(task.getId()); + this.workflowAPI.saveComment(taskComment); + return new ResponseEntityWorkflowCommentView( + toWorkflowTimelineItemView(taskComment)); + } + + throw new DoesNotExistException("Contentlet with identifier " + contentletId + " does not exist."); + } } // E:O:F:WorkflowResource. diff --git a/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json b/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json index 181ce16938e5..dc680b8c93d9 100644 --- a/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json +++ b/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "551fc73a-c338-498a-9aeb-b4f19b1708f9", + "_postman_id": "679e6657-065b-41a2-b666-672f59b9e95c", "name": "Workflow Resource Tests [/api/v1/workflows]", "description": "Test the necesary validations to every end point of the worlflow resource ", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -17773,6 +17773,289 @@ } }, "response": [] + }, + { + "name": "CreateComment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "// Test to check that the response has a 200 OK status", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// Test to check that the JSON response has the main properties", + "pm.test(\"Response has expected properties\", function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property(\"entity\");", + " pm.expect(jsonData).to.have.property(\"errors\");", + " pm.expect(jsonData).to.have.property(\"i18nMessagesMap\");", + " pm.expect(jsonData).to.have.property(\"messages\");", + " pm.expect(jsonData).to.have.property(\"pagination\");", + " pm.expect(jsonData).to.have.property(\"permissions\");", + "});", + "", + "// Test to check that \"entity\" has the expected properties", + "pm.test(\"Entity has expected properties\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.have.property(\"commentDescription\").that.is.a(\"string\");", + " pm.expect(entity).to.have.property(\"createdDate\").that.is.a(\"number\");", + " pm.expect(entity).to.have.property(\"postedBy\").that.is.a(\"string\");", + " pm.expect(entity).to.have.property(\"roleId\").that.is.a(\"string\");", + " pm.expect(entity).to.have.property(\"taskId\").that.is.a(\"string\");", + " pm.expect(entity).to.have.property(\"type\").that.is.a(\"string\");", + "", + " // Check specific values", + " pm.expect(entity.type).to.equal(\"WorkflowComment\");", + " pm.expect(entity.postedBy).to.equal(\"Admin User\");", + "});", + "", + "// Test to check that \"errors\" is an empty array", + "pm.test(\"Errors array is empty\", function () {", + " const errors = pm.response.json().errors;", + " pm.expect(errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"comment\":\"This is a postman test\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/{{identifier}}/comments", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "{{identifier}}", + "comments" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Resetable", + "item": [ + { + "name": "CreateContentPreconditions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 \", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "var jsonData = pm.response.json();", + "pm.collectionVariables.set(\"contentletIdentifier\", jsonData.entity.identifier);", + "pm.collectionVariables.set(\"contentletInode\", jsonData.entity.inode);", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "packages": {}, + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"contentlet\": {\n \"contentType\": \"webPageContent\",\n \"title\": \"Test Title\",\n \"body\": \"Test Body\",\n \"contentHost\": \"default\"\n }\n}" + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ] + }, + "description": "Fire any action using the actionId\n\nOptional: If you pass ?inode={inode}, you don't need body here.\n\n@Path(\"/actions/{actionId}/fire\")" + }, + "response": [] + }, + { + "name": "CheckResetable", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Test para verificar que la respuesta tiene el estado 200 OK", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// Test para verificar que el JSON de respuesta tiene las propiedades principales", + "pm.test(\"Response has expected properties\", function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property(\"entity\");", + " pm.expect(jsonData).to.have.property(\"errors\");", + " pm.expect(jsonData).to.have.property(\"i18nMessagesMap\");", + " pm.expect(jsonData).to.have.property(\"messages\");", + " pm.expect(jsonData).to.have.property(\"pagination\");", + " pm.expect(jsonData).to.have.property(\"permissions\");", + "});", + "", + "// Test para verificar que \"entity\" es un array y tiene al menos un elemento", + "pm.test(\"Entity is an array and contains items\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.be.an(\"array\").that.is.not.empty;", + "});", + "", + "// Test para verificar las propiedades de los objetos dentro de \"entity\"", + "pm.test(\"Each entity item has expected properties\", function () {", + " const entity = pm.response.json().entity;", + " entity.forEach(item => {", + " pm.expect(item).to.have.property(\"actionInputs\").that.is.an(\"array\");", + " pm.expect(item).to.have.property(\"assignable\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"commentable\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"condition\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"hasArchiveActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasDeleteActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasDestroyActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasMoveActionletActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasMoveActionletHasPathActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasOnlyBatchActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasPublishActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasPushPublishActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasSaveActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasUnarchiveActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"hasUnpublishActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"icon\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"id\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"name\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"nextAssign\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"nextStep\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"nextStepCurrentStep\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"order\").that.is.a(\"number\");", + " pm.expect(item).to.have.property(\"owner\").that.is.null;", + " pm.expect(item).to.have.property(\"hasResetActionlet\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"roleHierarchyForAssign\").that.is.a(\"boolean\");", + " pm.expect(item).to.have.property(\"schemeId\").that.is.a(\"string\");", + " pm.expect(item).to.have.property(\"showOn\").that.is.an(\"array\");", + " ", + " // Verificación adicional: que \"showOn\" contiene valores específicos esperados", + " const allowedShowOnValues = [\"NEW\", \"LISTING\", \"PUBLISHED\", \"UNLOCKED\", \"ARCHIVED\", \"UNPUBLISHED\", \"EDITING\", \"LOCKED\"];", + " item.showOn.forEach(status => {", + " pm.expect(allowedShowOnValues).to.include(status);", + " });", + " });", + "});", + "", + "// Test para verificar que \"errors\" es un array vacío", + "pm.test(\"Errors array is empty\", function () {", + " const errors = pm.response.json().errors;", + " pm.expect(errors).to.be.an(\"array\").that.is.empty;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/workflow/contentlet/{{contentletInode}}/actions?renderMode=EDITING", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "contentlet", + "{{contentletInode}}", + "actions" + ], + "query": [ + { + "key": "renderMode", + "value": "EDITING" + } + ] + } + }, + "response": [] } ] }, @@ -18254,4 +18537,4 @@ "value": "" } ] -} \ No newline at end of file +}