diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java index fc4ee59c4b79..fc9d31054f5a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java @@ -269,7 +269,18 @@ public interface VersionableAPI { * @throws DotSecurityException */ boolean isWorking(Versionable versionable) throws DotDataException, DotStateException,DotSecurityException; - + + /** + * Tells if has working version in any language. + * + * @param versionable + * @return true if it has the working version. False if not + * @throws DotDataException + * @throws DotStateException + * @throws DotSecurityException + */ + boolean hasWorkingVersionInAnyOtherLanguage(Versionable versionable, long versionableLanguageId) throws DotDataException, DotStateException,DotSecurityException; + /** * Sets the versionable as the working version for its identifier * diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java index 199b0ab8c542..fe51deb90788 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -15,6 +15,7 @@ import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Identifier; import com.dotmarketing.beans.VersionInfo; +import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.contentlet.model.Contentlet; @@ -350,6 +351,24 @@ public boolean isLocked(final Versionable versionable) throws DotDataException, } } + @CloseDBIfOpened + @Override + public boolean hasWorkingVersionInAnyOtherLanguage(Versionable versionable, final long versionableLanguageId) throws DotDataException, DotStateException, DotSecurityException { + + if(!UtilMethods.isSet(versionable) || !InodeUtils.isSet(versionable.getVersionId())) { + return false; + } + + final Identifier identifier = APILocator.getIdentifierAPI().find(versionable); + if(identifier==null || !UtilMethods.isSet(identifier.getId()) || !UtilMethods.isSet(identifier.getAssetType())) { + return false; + } + + // only contents are multi language + return "contentlet".equals(identifier.getAssetType())? + !this.versionableFactory.getWorkingVersionsExcludingLanguage(identifier.getId(), versionableLanguageId).isEmpty():false; + } + @CloseDBIfOpened @Override public boolean isWorking(final Versionable versionable) throws DotDataException, DotStateException, DotSecurityException { diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactory.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactory.java index f55ee8ad2f4c..c98a4d81f2f9 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactory.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactory.java @@ -2,6 +2,7 @@ import com.dotcms.variant.model.Variant; import java.util.List; +import java.util.Map; import java.util.Optional; import com.dotmarketing.beans.Identifier; @@ -108,6 +109,15 @@ public abstract class VersionableFactory { */ protected abstract VersionInfo getVersionInfo(String identifier) throws DotDataException, DotStateException; + /** + * Get a list of all the working versions of a contentlet excluding the one with the specified language id. + * @param identifier + * @param lang + * @return List of the rows (working_inode + lang) + * @throws DotDataException + */ + protected abstract List> getWorkingVersionsExcludingLanguage(String identifier, long lang) throws DotDataException; + /** * * @param identifier diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactoryImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactoryImpl.java index d515816dbd08..4b215ef5fb88 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableFactoryImpl.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -239,6 +240,13 @@ protected List findAllVersions(final String id, final Optional> getWorkingVersionsExcludingLanguage(final String identifier, final long lang) throws DotDataException { + + return new DotConnect().setSQL("select working_inode, lang from contentlet_version_info where identifier = ? and lang != ?") + .addParam(identifier).addParam(lang).loadObjectResults(); + } + @Override protected VersionInfo getVersionInfo(String identifier) throws DotDataException, DotStateException { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java index cbebf07c0965..4558afc66a56 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/workflows/business/WorkflowAPIImpl.java @@ -3530,7 +3530,8 @@ public void validateActionStepAndWorkflow(final Contentlet contentlet, final Use try { final boolean isValidContentlet = !InodeUtils.isSet(contentlet.getInode()) - || contentlet.isWorking(); + || contentlet.isWorking() || + APILocator.getVersionableAPI().hasWorkingVersionInAnyOtherLanguage(contentlet, contentlet.getLanguageId()); if (!isValidContentlet) { throw new IllegalArgumentException(LanguageUtil diff --git a/dotcms-integration/src/test/java/com/dotmarketing/business/VersionableAPITest.java b/dotcms-integration/src/test/java/com/dotmarketing/business/VersionableAPITest.java index 4f0121367701..7e44e1cabeab 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/business/VersionableAPITest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/business/VersionableAPITest.java @@ -31,6 +31,8 @@ import java.util.List; import java.util.Random; + +import graphql.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -488,4 +490,100 @@ public void getEmptyVariant() throws DotDataException { assertTrue(allByVariant.isEmpty()); } + + /** + * Method to test: {@link VersionableAPI#isWorking(Versionable)} + * When: Create a {@link Contentlet} but do not save it, call the isWorking + * Should: should false + */ + @Test + public void test_is_working_with_non_persisted_contentlet() throws DotDataException, DotSecurityException { + + final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent"); + final Contentlet myContentlet = new ContentletDataGen(contentType).next(); + + Assert.assertFalse(myContentlet.isWorking()); + } + + /** + * Method to test: {@link VersionableAPI#isWorking(Versionable)} + * When: Create a {@link Contentlet} and save it, call the isWorking + * Should: should true + */ + @Test + public void test_is_working_with_persisted_contentlet() throws DotDataException, DotSecurityException { + + final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent"); + final Contentlet myContentlet = new ContentletDataGen(contentType) + .setProperty("title","Test").setProperty("body","Test Body") + .nextPersisted(); + + Assert.assertTrue(myContentlet.isWorking()); + } + + /** + * Method to test: {@link VersionableAPI#isWorking(Versionable)} + * When: Create a {@link Contentlet} and save it, call the isWorking + * Next, create another languages. + * Next, create a version but in that new language, call the isWorking + * Should: should false + */ + @Test + public void test_is_working_with_persisted_in_diff_lang_contentlet() throws DotDataException, DotSecurityException { + + final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent"); + final Contentlet myContentlet = new ContentletDataGen(contentType) + .setProperty("title","Test").setProperty("body","Test Body") + .nextPersisted(); + final String countryCode = "it"; + final String languageCode = "it"; + final Language languageIt = APILocator.getLanguageAPI().getLanguage(languageCode, countryCode)==null? + new LanguageDataGen().countryCode(countryCode).languageCode(languageCode).nextPersisted(): + APILocator.getLanguageAPI().getLanguage(languageCode, countryCode); + + final Contentlet myContentletIt = new ContentletDataGen(contentType) + .setProperty("title","Test").setProperty("body","Test Body") + .languageId(languageIt.getId()) + .next(); + + Assert.assertTrue(myContentlet.isWorking()); + + myContentletIt.setIdentifier(myContentlet.getIdentifier()); + + Assert.assertFalse(myContentletIt.isWorking()); + } + + /** + * Method to test: {@link VersionableAPI#isWorking(Versionable)} + * When: Create a {@link Contentlet} and save it, call the isWorking + * Next, create another languages. + * Next, create a version but in that new language, call the isWorking + * Additionally calls the {@link VersionableAPI#hasWorkingVersionInAnyOtherLanguage(Versionable, long)} that may return true + */ + @Test + public void test_is_working_with_persisted_in_any_lang_contentlet() throws DotDataException, DotSecurityException { + + final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent"); + final Contentlet myContentlet = new ContentletDataGen(contentType) + .setProperty("title","Test").setProperty("body","Test Body") + .nextPersisted(); + final String countryCode = "it"; + final String languageCode = "it"; + final Language languageIt = APILocator.getLanguageAPI().getLanguage(languageCode, countryCode)==null? + new LanguageDataGen().countryCode(countryCode).languageCode(languageCode).nextPersisted(): + APILocator.getLanguageAPI().getLanguage(languageCode, countryCode); + + final Contentlet myContentletIt = new ContentletDataGen(contentType) + .setProperty("title","Test").setProperty("body","Test Body") + .languageId(languageIt.getId()) + .next(); + + Assert.assertTrue(myContentlet.isWorking()); + + myContentletIt.setIdentifier(myContentlet.getIdentifier()); + + Assert.assertFalse(myContentletIt.isWorking()); + + Assert.assertTrue(APILocator.getVersionableAPI().hasWorkingVersionInAnyOtherLanguage(myContentletIt, languageIt.getId())); + } } 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 008718ee28fb..bb61a687a986 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": "769562e3-1639-4158-b011-3a6e3d68cf06", + "_postman_id": "5ccc1142-2412-44f1-a682-d9ee757e8ecb", "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", @@ -18227,6 +18227,222 @@ "response": [] } ] + }, + { + "name": "SavingOnDiffLanguageByActionId", + "item": [ + { + "name": "CreateFrenchType", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "", + "pm.collectionVariables.set(\"frenchLanguageId\", jsonData.entity.id);", + "pm.test(\"Status code should be ok 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"languageCode\":\"fr\",\n \"countryCode\":\"fr\",\n \"language\":\"French\",\n \"country\":\"French\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v2/languages", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v2", + "languages" + ] + } + }, + "response": [] + }, + { + "name": "CreateEnglishContent", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = pm.response.json();", + "", + "pm.collectionVariables.set(\"englishidentifier\", jsonData.entity.identifier);", + "pm.test(\"Status code should be ok 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Check that the Contentlet's english languageId\", function () {", + " pm.expect(jsonData.entity.languageId).to.eql(1);", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contentlet\":{\n \"contentType\":\"webPageContent\",\n \"title\":\"TestEnglishFrench\",\n \"contentHost\":\"default\",\n \"body\":\"Body Test\",\n \"languageId\":1\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR ", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ], + "query": [ + { + "key": "indexPolicy", + "value": "WAIT_FOR " + } + ] + } + }, + "response": [] + }, + { + "name": "CreateFrenchContent", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let jsonData = pm.response.json();", + "", + "pm.test(\"Status code should be ok 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "let englishidentifier = pm.collectionVariables.get(\"englishidentifier\");", + "let frenchLanguageId = pm.collectionVariables.get(\"frenchLanguageId\");", + "", + "pm.test(\"Check that the Contentlet's french id\", function () {", + " pm.expect(jsonData.entity.identifier).to.eql(englishidentifier);", + "});", + "", + "pm.test(\"Check that the Contentlet's french languageId\", function () {", + " pm.expect(jsonData.entity.languageId).to.eql(frenchLanguageId);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contentlet\":{\n \"identifier\":\"{{englishidentifier}}\",\n \"languageId\":\"{{frenchLanguageId}}\",\n \"contentType\":\"webPageContent\",\n \"title\":\"TestEnglishFrench\",\n \"contentHost\":\"default\",\n \"body\":\"Corps d essai\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/ceca71a0-deee-4999-bd47-b01baa1bcfc8/fire?identifier={{englishidentifier}}&lang=1&indexPolicy=WAIT_FOR ", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "ceca71a0-deee-4999-bd47-b01baa1bcfc8", + "fire" + ], + "query": [ + { + "key": "identifier", + "value": "{{englishidentifier}}" + }, + { + "key": "lang", + "value": "1" + }, + { + "key": "indexPolicy", + "value": "WAIT_FOR " + } + ] + } + }, + "response": [] + } + ], + "description": "Creates a French language and them creates a rich text in english and finally the french version of the english one previously created\n\nshould has the same id but diff language\n\nSince will use the system working, uses the safe action to creates the contentlets" } ], "event": [ @@ -18329,6 +18545,10 @@ { "key": "identifier", "value": "" + }, + { + "key": "frenchLanguageId", + "value": "" } ] -} +} \ No newline at end of file