diff --git a/.gitignore b/.gitignore index 871ccdc2..b1a5dd27 100755 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,6 @@ composer.lock /tests/_craft/storage /tests/_support/_generated -docker-compose.override.yml \ No newline at end of file +docker-compose.override.yml + +e2e/cypress/screenshots diff --git a/CHANGELOG.md b/CHANGELOG.md index afb4ae47..51e765e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 4.3.0 - 2023-10-04 +### Added +- Queue each translation file transfer separately + ## 4.2.2 - 2023-09-22 ### Fixed - Empty user id on draft creation diff --git a/composer.json b/composer.json index e9b44aa3..610a52ff 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "lilt/craft-lilt-plugin", "description": "The Lilt plugin makes it easy for you to send content to Lilt for translation right from within Craft CMS.", "type": "craft-plugin", - "version": "4.2.2", + "version": "4.3.0", "keywords": [ "craft", "cms", diff --git a/e2e/cypress/e2e/jobs/instant/success-path-multiple-disable-split-send.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-multiple-disable-split-send.cy.js new file mode 100644 index 00000000..f3798074 --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-multiple-disable-split-send.cy.js @@ -0,0 +1,24 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with multiple target languages and disabled split send', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + splitSend: false + }); + }); + }); diff --git a/e2e/cypress/e2e/jobs/instant/success-path-single-copy-slug-and-enable-after-publish.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-single-copy-slug-and-enable-after-publish.cy.js new file mode 100644 index 00000000..3fbf2c5d --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-single-copy-slug-and-enable-after-publish.cy.js @@ -0,0 +1,25 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.log(`Job title: ${jobTitle}`) + cy.log(`Slug: ${slug}`) + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: true, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/instant/success-path-single-copy-slug.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-single-copy-slug.cy.js new file mode 100644 index 00000000..ca6cdf9f --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-single-copy-slug.cy.js @@ -0,0 +1,22 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: false, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/instant/success-path-single-disable-split-send.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-single-disable-split-send.cy.js new file mode 100644 index 00000000..a2d88acf --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-single-disable-split-send.cy.js @@ -0,0 +1,23 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ["de"], + splitSend: false, + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/instant/success-path-single-enable-after-publish.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-single-enable-after-publish.cy.js new file mode 100644 index 00000000..1ef86842 --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-single-enable-after-publish.cy.js @@ -0,0 +1,22 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: true, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing-disable-split-send.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing-disable-split-send.cy.js new file mode 100644 index 00000000..0ff2a418 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing-disable-split-send.cy.js @@ -0,0 +1,25 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with multiple target languages with bulk publishing and disabled split send', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + splitSend: false + }); + }); + + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single-copy-slug-and-enable-after-publish.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single-copy-slug-and-enable-after-publish.cy.js new file mode 100644 index 00000000..f0b6c9e8 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-single-copy-slug-and-enable-after-publish.cy.js @@ -0,0 +1,25 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.log(`Job title: ${jobTitle}`) + cy.log(`Slug: ${slug}`) + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: true, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single-copy-slug.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single-copy-slug.cy.js new file mode 100644 index 00000000..eb3abc52 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-single-copy-slug.cy.js @@ -0,0 +1,22 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: false, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single-disable-split-send.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single-disable-split-send.cy.js new file mode 100644 index 00000000..2e9a6414 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-single-disable-split-send.cy.js @@ -0,0 +1,23 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ["de"], + splitSend: false + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single-enable-after-publish.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single-enable-after-publish.cy.js new file mode 100644 index 00000000..2b5cc95d --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-single-enable-after-publish.cy.js @@ -0,0 +1,22 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: true, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js index d1d66d9c..cf4b2b1c 100644 --- a/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js +++ b/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js @@ -19,49 +19,4 @@ describe( languages: ["de"], }) }); - - it('with copy slug disabled & enable after publish enabled', () => { - const {jobTitle, slug} = generateJobData(); - - cy.verifiedFlow({ - slug, - entryLabel, - jobTitle, - entryId, - copySlug: false, - enableAfterPublish: true, - languages: ["de"] - }) - }); - - it('with copy slug enabled & enable after publish disabled', () => { - const {jobTitle, slug} = generateJobData(); - - cy.verifiedFlow({ - slug, - entryLabel, - jobTitle, - entryId, - copySlug: true, - enableAfterPublish: false, - languages: ["de"] - }) - }); - - it('with copy slug enabled & enable after publish enabled', () => { - const {jobTitle, slug} = generateJobData(); - - cy.log(`Job title: ${jobTitle}`) - cy.log(`Slug: ${slug}`) - - cy.verifiedFlow({ - slug, - entryLabel, - jobTitle, - entryId, - copySlug: true, - enableAfterPublish: true, - languages: ["de"] - }) - }); }); diff --git a/e2e/cypress/support/commands.js b/e2e/cypress/support/commands.js index 50ed675d..1dc4bc9b 100644 --- a/e2e/cypress/support/commands.js +++ b/e2e/cypress/support/commands.js @@ -91,9 +91,13 @@ Cypress.Commands.add('setConfigurationOption', (option, enabled) => { const options = { enableEntries: { id: 'enableEntriesForTargetSites', - }, copySlug: { + }, + copySlug: { id: 'copyEntriesSlugFromSourceToTarget', }, + splitSend: { + id: 'queueEachTranslationFileSeparately', + }, }; if (!options[option]) { @@ -145,7 +149,7 @@ Cypress.Commands.add('setConfigurationOption', (option, enabled) => { cy.get('#connectorApiUrl').clear().type(apiUrl); } - if(apiKey) { + if (apiKey) { cy.get('#connectorApiKey').clear().type('this_is_apy_key'); } @@ -387,30 +391,42 @@ Cypress.Commands.add('waitForJobStatus', * * @memberof cy * @method waitForTranslationDrafts + * @param {string} jobTitle * @param {int} maxAttempts * @param {int} attempts * @param {int} waitPerIteration * @returns undefined */ Cypress.Commands.add('waitForTranslationDrafts', - (maxAttempts = 100, attempts = 0, waitPerIteration = 1000) => { - if (attempts > maxAttempts) { - throw new Error('Timed out waiting for report to be generated'); - } - cy.get('#translations-list th[data-title="Translation"] div.element'). - invoke('attr', 'data-translated-draft-id'). - then(async translatedDraftId => { - cy.log('TransladtedDraftId'); - cy.log(translatedDraftId); + (jobTitle, maxAttempts = 100, attempts = 0, waitPerIteration = 1000) => { + if (attempts > maxAttempts) { + throw new Error('Timed out waiting for translation drafts'); + } + cy.clearCraftCache(); + cy.openJob(jobTitle); - if (translatedDraftId === '' || translatedDraftId === undefined) { - cy.wait(waitPerIteration); - cy.reload(); - cy.waitForTranslationDrafts(maxAttempts, attempts + 1, - waitPerIteration); - } - }); + cy.get('#translations-list th[data-title="Translation"] div.element').then($elements => { + // Filter out elements that have a data-translated-draft-id attribute + const elementsWithoutDraftId = $elements.filter((index, element) => { + return !element.getAttribute('data-translated-draft-id'); + }); + + cy.log(`Checked ${$elements.length} elements, found ${elementsWithoutDraftId.length} without a draft id.`); + + if (elementsWithoutDraftId.length > 0) { + cy.log(`empty translation draft id`); + cy.wait(waitPerIteration); + cy.clearAllLocalStorage(); + cy.reload(true); + cy.waitForTranslationDrafts( + jobTitle, + maxAttempts, + attempts + 1, + waitPerIteration + ); + } }); + }); /** * Publish single translation for job @@ -676,19 +692,14 @@ Cypress.Commands.add('copySourceTextFlow', ({ cy.get('#author-label').invoke('text').should('equal', 'Author'); - cy.get('#meta-settings-source-site'). - invoke('text'). - should('equal', 'en'); + cy.get('#meta-settings-source-site').invoke('text').should('equal', 'en'); for (const language of languages) { cy.get( - `#meta-settings-target-sites .target-languages-list span[data-language="${language}"]`). - should('be.visible'); + `#meta-settings-target-sites .target-languages-list span[data-language="${language}"]`).should('be.visible'); } - cy.get('#meta-settings-translation-workflow'). - invoke('text'). - should('equal', 'Copy source text'); + cy.get('#meta-settings-translation-workflow').invoke('text').should('equal', 'Copy source text'); cy.get('#meta-settings-job-id'). invoke('text'). @@ -855,3 +866,14 @@ Cypress.Commands.add('releaseQueueManager', () => { } }); }); + +/** + * @memberof cy + * @method clearCraftCache + * @returns undefined + */ +Cypress.Commands.add('clearCraftCache', () => { + const appUrl = Cypress.env('APP_URL'); + cy.visit(`${appUrl}/admin/utilities/clear-caches`); + cy.contains('button', 'Clear caches').click(); +}); diff --git a/e2e/cypress/support/flow/instant.js b/e2e/cypress/support/flow/instant.js index c2eeb107..3c4d9216 100644 --- a/e2e/cypress/support/flow/instant.js +++ b/e2e/cypress/support/flow/instant.js @@ -16,6 +16,7 @@ Cypress.Commands.add('instantFlow', ({ languages = ['de'], batchPublishing = false, //publish all translations at once with publish button entryId = 24, + splitSend = true, }) => { const isMockserverEnabled = Cypress.env('MOCKSERVER_ENABLED'); @@ -25,6 +26,7 @@ Cypress.Commands.add('instantFlow', ({ cy.setConfigurationOption('enableEntries', enableAfterPublish); cy.setConfigurationOption('copySlug', copySlug); + cy.setConfigurationOption('splitSend', splitSend); if (copySlug) { // update slug on entry and enable slug copy option @@ -46,6 +48,82 @@ Cypress.Commands.add('instantFlow', ({ if (isMockserverEnabled) { cy.wrap(mockServerClient.reset()); + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/jobs/777', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'id': 777, + 'status': 'complete', + 'errorMsg': '', + 'createdAt': '2019-08-24T14:15:22Z', + 'updatedAt': '2019-08-24T14:15:22Z', + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + + let translationsResult = []; + + for (const language of languages) { + const siteId = siteLanguages[language]; + const translationId = 777000 + siteId; + + const translationResult = { + 'createdAt': '2022-05-29T11:31:58', + 'errorMsg': null, + 'id': translationId, + 'name': '777_element_' + 24 + '_slug_for_' + language + '.json+html', + 'status': 'mt_complete', + 'trgLang': language, + 'trgLocale': '', + 'updatedAt': '2022-06-02T23:01:42', + }; + translationsResult.push(translationResult); + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', + 'path': `/translations/${translationId}`, + 'headers': [ + { + 'name': 'Authorization', + 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify(translationResult), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + } + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/translations', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], 'queryStringParameters': [ + { + 'name': 'limit', 'values': ['100'], + }, { + 'name': 'start', 'values': ['00'], + }, { + 'name': 'job_id', 'values': ['777'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'limit': 100, 'start': 0, 'results': translationsResult, + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + cy.wrap(mockServerClient.mockAnyResponse({ 'httpRequest': { 'method': 'GET', 'path': '/settings', @@ -128,85 +206,14 @@ Cypress.Commands.add('instantFlow', ({ invoke('text'). should('contain', 'In Progress'); - cy.waitForTranslationDrafts(); + cy.waitForTranslationDrafts( + jobTitle, + 100, + 0, + 1000 + ); if (isMockserverEnabled) { - cy.wrap(mockServerClient.mockAnyResponse({ - 'httpRequest': { - 'method': 'GET', 'path': '/jobs/777', 'headers': [ - { - 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], - }], - }, 'httpResponse': { - 'statusCode': 200, 'body': JSON.stringify({ - 'id': 777, - 'status': 'complete', - 'errorMsg': '', - 'createdAt': '2019-08-24T14:15:22Z', - 'updatedAt': '2019-08-24T14:15:22Z', - }), - }, 'times': { - 'remainingTimes': 1, 'unlimited': false, - }, - })); - - let translationsResult = []; - - for (const language of languages) { - const siteId = siteLanguages[language]; - const translationId = 777000 + siteId; - - const translationResult = { - 'createdAt': '2022-05-29T11:31:58', - 'errorMsg': null, - 'id': translationId, - 'name': '777_element_' + 24 + '_slug_for_' + language + '.json+html', - 'status': 'mt_complete', - 'trgLang': language, - 'trgLocale': '', - 'updatedAt': '2022-06-02T23:01:42', - }; - translationsResult.push(translationResult); - - cy.wrap(mockServerClient.mockAnyResponse({ - 'httpRequest': { - 'method': 'GET', - 'path': `/translations/${translationId}`, - 'headers': [ - { - 'name': 'Authorization', - 'values': ['Bearer this_is_apy_key'], - }], - }, 'httpResponse': { - 'statusCode': 200, 'body': JSON.stringify(translationResult), - }, 'times': { - 'remainingTimes': 1, 'unlimited': false, - }, - })); - } - - cy.wrap(mockServerClient.mockAnyResponse({ - 'httpRequest': { - 'method': 'GET', 'path': '/translations', 'headers': [ - { - 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], - }], 'queryStringParameters': [ - { - 'name': 'limit', 'values': ['100'], - }, { - 'name': 'start', 'values': ['00'], - }, { - 'name': 'job_id', 'values': ['777'], - }], - }, 'httpResponse': { - 'statusCode': 200, 'body': JSON.stringify({ - 'limit': 100, 'start': 0, 'results': translationsResult, - }), - }, 'times': { - 'remainingTimes': 1, 'unlimited': false, - }, - })); - for (const language of languages) { cy.get( `#translations-list th[data-title="Translation"] div.element[data-target-site-language="${language}"]`). diff --git a/e2e/cypress/support/flow/verified.js b/e2e/cypress/support/flow/verified.js index 04521eb3..ba0bf81a 100644 --- a/e2e/cypress/support/flow/verified.js +++ b/e2e/cypress/support/flow/verified.js @@ -17,6 +17,7 @@ Cypress.Commands.add('verifiedFlow', ({ languages = ['de'], batchPublishing = false, //publish all translations at once with publish button entryId = 24, + splitSend = true, }) => { const isMockserverEnabled = Cypress.env('MOCKSERVER_ENABLED'); @@ -26,6 +27,7 @@ Cypress.Commands.add('verifiedFlow', ({ cy.setConfigurationOption('enableEntries', enableAfterPublish); cy.setConfigurationOption('copySlug', copySlug); + cy.setConfigurationOption('splitSend', splitSend); if (copySlug) { // update slug on entry and enable slug copy option @@ -43,10 +45,85 @@ Cypress.Commands.add('verifiedFlow', ({ let mockServerClient = mockServer.mockServerClient( Cypress.env('MOCKSERVER_HOST'), Cypress.env('MOCKSERVER_PORT')); - if (isMockserverEnabled) { cy.wrap(mockServerClient.reset()); + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/jobs/777', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'id': 777, + 'status': 'complete', + 'errorMsg': '', + 'createdAt': '2019-08-24T14:15:22Z', + 'updatedAt': '2019-08-24T14:15:22Z', + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + + let translationsResult = []; + + for (const language of languages) { + const siteId = siteLanguages[language]; + const translationId = 777000 + siteId; + + const translationResult = { + 'createdAt': '2022-05-29T11:31:58', + 'errorMsg': null, + 'id': translationId, + 'name': '777_element_' + 24 + '_slug_for_' + language + '.json+html', + 'status': 'export_complete', + 'trgLang': language, + 'trgLocale': '', + 'updatedAt': '2022-06-02T23:01:42', + }; + translationsResult.push(translationResult); + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', + 'path': `/translations/${translationId}`, + 'headers': [ + { + 'name': 'Authorization', + 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify(translationResult), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + } + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/translations', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], 'queryStringParameters': [ + { + 'name': 'limit', 'values': ['100'], + }, { + 'name': 'start', 'values': ['00'], + }, { + 'name': 'job_id', 'values': ['777'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'limit': 100, 'start': 0, 'results': translationsResult, + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + cy.wrap(mockServerClient.mockAnyResponse({ 'httpRequest': { 'method': 'GET', 'path': '/settings', @@ -129,85 +206,14 @@ Cypress.Commands.add('verifiedFlow', ({ invoke('text'). should('contain', 'In Progress'); - cy.waitForTranslationDrafts(); + cy.waitForTranslationDrafts( + jobTitle, + 100, + 0, + 1000 + ); if (isMockserverEnabled) { - cy.wrap(mockServerClient.mockAnyResponse({ - 'httpRequest': { - 'method': 'GET', 'path': '/jobs/777', 'headers': [ - { - 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], - }], - }, 'httpResponse': { - 'statusCode': 200, 'body': JSON.stringify({ - 'id': 777, - 'status': 'complete', - 'errorMsg': '', - 'createdAt': '2019-08-24T14:15:22Z', - 'updatedAt': '2019-08-24T14:15:22Z', - }), - }, 'times': { - 'remainingTimes': 1, 'unlimited': false, - }, - })); - - let translationsResult = []; - - for (const language of languages) { - const siteId = siteLanguages[language]; - const translationId = 777000 + siteId; - - const translationResult = { - 'createdAt': '2022-05-29T11:31:58', - 'errorMsg': null, - 'id': translationId, - 'name': '777_element_' + 24 + '_slug_for_' + language + '.json+html', - 'status': 'export_complete', - 'trgLang': language, - 'trgLocale': '', - 'updatedAt': '2022-06-02T23:01:42', - }; - translationsResult.push(translationResult); - - cy.wrap(mockServerClient.mockAnyResponse({ - 'httpRequest': { - 'method': 'GET', - 'path': `/translations/${translationId}`, - 'headers': [ - { - 'name': 'Authorization', - 'values': ['Bearer this_is_apy_key'], - }], - }, 'httpResponse': { - 'statusCode': 200, 'body': JSON.stringify(translationResult), - }, 'times': { - 'remainingTimes': 1, 'unlimited': false, - }, - })); - } - - cy.wrap(mockServerClient.mockAnyResponse({ - 'httpRequest': { - 'method': 'GET', 'path': '/translations', 'headers': [ - { - 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], - }], 'queryStringParameters': [ - { - 'name': 'limit', 'values': ['100'], - }, { - 'name': 'start', 'values': ['00'], - }, { - 'name': 'job_id', 'values': ['777'], - }], - }, 'httpResponse': { - 'statusCode': 200, 'body': JSON.stringify({ - 'limit': 100, 'start': 0, 'results': translationsResult, - }), - }, 'times': { - 'remainingTimes': 1, 'unlimited': false, - }, - })); - for (const language of languages) { cy.get( `#translations-list th[data-title="Translation"] div.element[data-target-site-language="${language}"]`). diff --git a/src/Craftliltplugin.php b/src/Craftliltplugin.php index 73591968..34e1b34f 100644 --- a/src/Craftliltplugin.php +++ b/src/Craftliltplugin.php @@ -37,6 +37,7 @@ use lilthq\craftliltplugin\services\handlers\PublishDraftHandler; use lilthq\craftliltplugin\services\handlers\RefreshJobStatusHandler; use lilthq\craftliltplugin\services\handlers\SendJobToLiltConnectorHandler; +use lilthq\craftliltplugin\services\handlers\SendTranslationToLiltConnectorHandler; use lilthq\craftliltplugin\services\handlers\SyncJobFromLiltConnectorHandler; use lilthq\craftliltplugin\services\handlers\TranslationFailedHandler; use lilthq\craftliltplugin\services\handlers\UpdateJobStatusHandler; @@ -85,6 +86,7 @@ * @property CreateJobHandler $createJobHandler * @property EditJobHandler $editJobHandler * @property SendJobToLiltConnectorHandler $sendJobToLiltConnectorHandler + * @property SendTranslationToLiltConnectorHandler $sendTranslationToLiltConnectorHandler * @property SyncJobFromLiltConnectorHandler $syncJobFromLiltConnectorHandler * @property PublishDraftHandler $publishDraftsHandler * @property Configuration $connectorConfiguration diff --git a/src/controllers/PostConfigurationController.php b/src/controllers/PostConfigurationController.php index 25853fe1..cc6c381f 100644 --- a/src/controllers/PostConfigurationController.php +++ b/src/controllers/PostConfigurationController.php @@ -17,7 +17,6 @@ use lilthq\craftliltplugin\controllers\job\AbstractJobController; use lilthq\craftliltplugin\Craftliltplugin; use LiltConnectorSDK\Configuration as LiltConnectorConfiguration; -use lilthq\craftliltplugin\records\SettingRecord; use lilthq\craftliltplugin\services\repositories\SettingsRepository; use lilthq\craftliltplugin\utilities\Configuration; use Throwable; @@ -76,7 +75,7 @@ public function actionInvoke(): Response ); } - $liltConfigDisabled = (bool) $request->getBodyParam('liltConfigDisabled'); + $liltConfigDisabled = (bool)$request->getBodyParam('liltConfigDisabled'); if ($liltConfigDisabled) { Craft::$app->getSession()->setFlash( @@ -91,12 +90,22 @@ public function actionInvoke(): Response Craftliltplugin::getInstance()->settingsRepository->save( SettingsRepository::ENABLE_ENTRIES_FOR_TARGET_SITES, - $request->getBodyParam('enableEntriesForTargetSites') + $request->getBodyParam('enableEntriesForTargetSites') ?? '0' ); Craftliltplugin::getInstance()->settingsRepository->save( SettingsRepository::COPY_ENTRIES_SLUG_FROM_SOURCE_TO_TARGET, - $request->getBodyParam('copyEntriesSlugFromSourceToTarget') + $request->getBodyParam('copyEntriesSlugFromSourceToTarget') ?? '0' + ); + + $queueEachTranslationFileSeparately = $request->getBodyParam('queueEachTranslationFileSeparately'); + if (empty($queueEachTranslationFileSeparately)) { + $queueEachTranslationFileSeparately = 0; + } + + Craftliltplugin::getInstance()->settingsRepository->save( + SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY, + (string)$queueEachTranslationFileSeparately ); $settingsRequest = new SettingsRequest(); diff --git a/src/migrations/Install.php b/src/migrations/Install.php index f15016e5..4dcebac4 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -11,6 +11,8 @@ use craft\db\Migration; use lilthq\craftliltplugin\parameters\CraftliltpluginParameters; +use lilthq\craftliltplugin\records\SettingRecord; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; class Install extends Migration { @@ -168,6 +170,18 @@ public function safeUp(): void 'CASCADE', null ); + + $settingRecord = SettingRecord::findOne( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + if (!$settingRecord) { + $settingRecord = new SettingRecord( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + } + + $settingRecord->value = 1; + $settingRecord->save(); } /** diff --git a/src/migrations/m231001_174918_queue_each_translation_file_separately.php b/src/migrations/m231001_174918_queue_each_translation_file_separately.php new file mode 100644 index 00000000..9e5a716f --- /dev/null +++ b/src/migrations/m231001_174918_queue_each_translation_file_separately.php @@ -0,0 +1,55 @@ + SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + if (!$settingRecord) { + $settingRecord = new SettingRecord( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + } + + $settingRecord->value = 1; + return $settingRecord->save(); + } + + /** + * @inheritdoc + */ + public function safeDown() + { + $settingRecord = SettingRecord::findOne( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + if (!$settingRecord) { + $settingRecord = new SettingRecord( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + } + + $settingRecord->value = 0; + return $settingRecord->save(); + } +} diff --git a/src/modules/AbstractRetryJob.php b/src/modules/AbstractRetryJob.php index 141f95f4..ffd088b4 100644 --- a/src/modules/AbstractRetryJob.php +++ b/src/modules/AbstractRetryJob.php @@ -9,7 +9,10 @@ namespace lilthq\craftliltplugin\modules; +use Craft; use craft\queue\BaseJob; +use lilthq\craftliltplugin\elements\Job; +use lilthq\craftliltplugin\records\JobRecord; abstract class AbstractRetryJob extends BaseJob { @@ -32,5 +35,46 @@ abstract class AbstractRetryJob extends BaseJob * @return bool */ abstract public function canRetry(): bool; + abstract public function getRetryJob(): BaseJob; + + protected function getCommand(): ?Command + { + if (!Craft::$app->getMutex()->acquire($this->getMutexKey())) { + Craft::error(sprintf('Job %s is already processing job %d', __CLASS__, $this->jobId)); + + return null; + } + + $jobId = $this->jobId; + $job = Job::findOne(['id' => $jobId]); + + if (!$job) { + return null; + } + + $jobRecord = JobRecord::findOne(['id' => $jobId]); + + if (!$jobRecord) { + Craft::error(sprintf("Can't find JobRecord for job id: %d", $jobId)); + + return null; + } + + return new Command($job, $jobRecord); + } + + protected function release(): void + { + Craft::$app->getMutex()->release($this->getMutexKey()); + } + + protected function getMutexKey(): string + { + $additional = ''; + if ($this->hasProperty('translationId') && !empty($this->translationId)) { + $additional .= sprintf('_%s', $this->translationId); + } + return __CLASS__ . '_' . __FUNCTION__ . '_' . $this->jobId . $additional; + } } diff --git a/src/modules/Command.php b/src/modules/Command.php new file mode 100644 index 00000000..757f64c2 --- /dev/null +++ b/src/modules/Command.php @@ -0,0 +1,40 @@ +job = $job; + $this->jobRecord = $jobRecord; + } + + /** + * @return Job + */ + public function getJob(): Job + { + return $this->job; + } + + /** + * @return JobRecord + */ + public function getJobRecord(): JobRecord + { + return $this->jobRecord; + } +} diff --git a/src/modules/FetchTranslationFromConnector.php b/src/modules/FetchTranslationFromConnector.php index 05c409fe..3753032e 100644 --- a/src/modules/FetchTranslationFromConnector.php +++ b/src/modules/FetchTranslationFromConnector.php @@ -84,11 +84,11 @@ public function execute($queue): void Craft::error( sprintf( "Connector translation id is empty for translation:" - . "%d source site: &d target site: %d lilt job: %d", + . "%d source site: %d target site: %d lilt job: %d", $translationRecord->id, $translationRecord->sourceSiteId, $translationRecord->targetSiteId, - $job->liltJobId, + $job->liltJobId ) ); diff --git a/src/modules/SendTranslationToConnector.php b/src/modules/SendTranslationToConnector.php new file mode 100644 index 00000000..3bf8b1c5 --- /dev/null +++ b/src/modules/SendTranslationToConnector.php @@ -0,0 +1,197 @@ +getCommand(); + if (empty($command)) { + return; + } + + if (!$command->getJob()->isVerifiedFlow() && !$command->getJob()->isInstantFlow()) { + Craft::error( + sprintf( + "Job can't be proceed, incorrect flow %s: %d", + $command->getJob()->translationWorkflow, + $command->getJob()->id + ) + ); + + return; + } + + if (empty($command->getJob()->liltJobId)) { + Craft::error( + sprintf( + "Job can't be proceed, empty lilt id [%s]: %d", + $command->getJob()->translationWorkflow, + $command->getJob()->id + ) + ); + + return; + } + + $element = Craft::$app->elements->getElementById($this->versionId, null, $command->getJob()->sourceSiteId); + + $translationRecord = TranslationRecord::findOne(['id' => $this->translationId]); + + Craftliltplugin::getInstance() + ->sendTranslationToLiltConnectorHandler + ->send( + new SendTranslationCommand( + $this->elementId, + $this->versionId, + $this->targetSiteId, + $element, + $command->getJob()->liltJobId, + $command->getJob(), + $translationRecord + ) + ); + + $translations = Craftliltplugin::getInstance() + ->translationRepository + ->findByJobId($this->jobId); + + $sourceContents = array_map( + function (TranslationModel $translationModel) { + return $translationModel->sourceContent; + }, + $translations + ); + + if (!in_array(null, $sourceContents)) { + // All translations downloaded, let's start the job + Craftliltplugin::getInstance()->connectorJobRepository->start( + $command->getJob()->liltJobId + ); + + Craftliltplugin::getInstance()->jobLogsRepository->create( + $this->jobId, + Craft::$app->getUser()->getId(), + 'Job uploaded to Lilt Platform' + ); + + Queue::push( + (new FetchJobStatusFromConnector([ + 'jobId' => $command->getJob()->id, + 'liltJobId' => $command->getJob()->liltJobId, + ])), + FetchJobStatusFromConnector::PRIORITY, + 10 + ); + } + + $this->markAsDone($queue); + $this->release(); + } + + /** + * @inheritdoc + */ + protected function defaultDescription(): ?string + { + return Craft::t('app', 'Sending translation to lilt'); + } + + /** + * @param $queue + * @return void + */ + private function markAsDone($queue): void + { + $this->setProgress( + $queue, + 1, + Craft::t( + 'app', + 'Sending translation for jobId: {jobId} to lilt platform done', + [ + 'jobId' => $this->jobId, + ] + ) + ); + } + + public static function getDelay(): int + { + $envDelay = getenv('CRAFT_LILT_PLUGIN_QUEUE_DELAY_IN_SECONDS'); + if (!empty($envDelay) || $envDelay === '0') { + return (int)$envDelay; + } + + return self::DELAY_IN_SECONDS; + } + + public function canRetry(): bool + { + return $this->attempt < self::RETRY_COUNT; + } + + public function getRetryJob(): BaseJob + { + return new self([ + 'jobId' => $this->jobId, + 'translationId' => $this->translationId, + 'elementId' => $this->elementId, + 'versionId' => $this->versionId, + 'targetSiteId' => $this->targetSiteId, + 'attempt' => $this->attempt + 1 + ]); + } +} diff --git a/src/parameters/CraftliltpluginParameters.php b/src/parameters/CraftliltpluginParameters.php index 88246c2a..6686965b 100755 --- a/src/parameters/CraftliltpluginParameters.php +++ b/src/parameters/CraftliltpluginParameters.php @@ -76,7 +76,6 @@ class CraftliltpluginParameters public const TRANSLATION_WORKFLOW_VERIFIED = SettingsResponse::LILT_TRANSLATION_WORKFLOW_VERIFIED; public const TRANSLATION_WORKFLOW_COPY_SOURCE_TEXT = 'COPY_SOURCE_TEXT'; - public static function getTranslationWorkflows(): array { return [ diff --git a/src/services/ServiceInitializer.php b/src/services/ServiceInitializer.php index 8c382244..e2317212 100644 --- a/src/services/ServiceInitializer.php +++ b/src/services/ServiceInitializer.php @@ -36,6 +36,7 @@ use lilthq\craftliltplugin\services\handlers\PublishDraftHandler; use lilthq\craftliltplugin\services\handlers\RefreshJobStatusHandler; use lilthq\craftliltplugin\services\handlers\SendJobToLiltConnectorHandler; +use lilthq\craftliltplugin\services\handlers\SendTranslationToLiltConnectorHandler; use lilthq\craftliltplugin\services\handlers\SyncJobFromLiltConnectorHandler; use lilthq\craftliltplugin\services\handlers\TranslationFailedHandler; use lilthq\craftliltplugin\services\handlers\UpdateJobStatusHandler; @@ -275,6 +276,32 @@ function () { ], ]); + $pluginInstance->setComponents([ + 'sendJobToLiltConnectorHandler' => function () use ($pluginInstance) { + return new SendJobToLiltConnectorHandler( + $pluginInstance->connectorJobRepository, + $pluginInstance->jobLogsRepository, + $pluginInstance->translationRepository, + $pluginInstance->languageMapper, + $pluginInstance->sendTranslationToLiltConnectorHandler, + $pluginInstance->settingsRepository + ); + } + ]); + + $pluginInstance->setComponents([ + 'sendTranslationToLiltConnectorHandler' => function () use ($pluginInstance) { + return new SendTranslationToLiltConnectorHandler( + $pluginInstance->jobLogsRepository, + $pluginInstance->translationRepository, + $pluginInstance->connectorJobsFileRepository, + $pluginInstance->createDraftHandler, + $pluginInstance->elementTranslatableContentProvider, + $pluginInstance->languageMapper + ); + } + ]); + $pluginInstance->listenerRegister->register(); $pluginInstance->loadI18NHandler->__invoke(); } diff --git a/src/services/handlers/SendJobToLiltConnectorHandler.php b/src/services/handlers/SendJobToLiltConnectorHandler.php index ffb84866..e4b0f599 100644 --- a/src/services/handlers/SendJobToLiltConnectorHandler.php +++ b/src/services/handlers/SendJobToLiltConnectorHandler.php @@ -12,22 +12,79 @@ use Craft; use craft\errors\ElementNotFoundException; use craft\helpers\Queue; -use DateTimeInterface; use LiltConnectorSDK\ApiException; -use LiltConnectorSDK\Model\JobResponse; -use lilthq\craftliltplugin\Craftliltplugin; use lilthq\craftliltplugin\elements\Job; -use lilthq\craftliltplugin\elements\Translation; use lilthq\craftliltplugin\modules\FetchJobStatusFromConnector; +use lilthq\craftliltplugin\modules\SendTranslationToConnector; use lilthq\craftliltplugin\records\JobRecord; use lilthq\craftliltplugin\records\TranslationRecord; -use lilthq\craftliltplugin\services\handlers\commands\CreateDraftCommand; +use lilthq\craftliltplugin\services\handlers\commands\SendTranslationCommand; +use lilthq\craftliltplugin\services\mappers\LanguageMapper; +use lilthq\craftliltplugin\services\repositories\external\ConnectorJobRepository; +use lilthq\craftliltplugin\services\repositories\JobLogsRepository; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; +use lilthq\craftliltplugin\services\repositories\TranslationRepository; use Throwable; use yii\base\Exception; use yii\db\StaleObjectException; class SendJobToLiltConnectorHandler { + /** + * @var ConnectorJobRepository + */ + public $connectorJobRepository; + + /** + * @var JobLogsRepository + */ + public $jobLogsRepository; + + /** + * @var TranslationRepository + */ + public $translationRepository; + + /** + * @var LanguageMapper + */ + public $languageMapper; + + /** + * @var SendTranslationToLiltConnectorHandler + */ + public $sendTranslationToLiltConnectorHandler; + + /** + * @var SettingsRepository + */ + public $settingsRepository; + + /** + * @param ConnectorJobRepository $connectorJobRepository + * @param JobLogsRepository $jobLogsRepository + * @param TranslationRepository $translationRepository + * @param LanguageMapper $languageMapper + * @param SendTranslationToLiltConnectorHandler $sendTranslationToLiltConnectorHandler + * @param SettingsRepository $settingsRepository + */ + public function __construct( + ConnectorJobRepository $connectorJobRepository, + JobLogsRepository $jobLogsRepository, + TranslationRepository $translationRepository, + LanguageMapper $languageMapper, + SendTranslationToLiltConnectorHandler $sendTranslationToLiltConnectorHandler, + SettingsRepository $settingsRepository + ) { + $this->connectorJobRepository = $connectorJobRepository; + $this->jobLogsRepository = $jobLogsRepository; + $this->translationRepository = $translationRepository; + $this->languageMapper = $languageMapper; + $this->sendTranslationToLiltConnectorHandler = $sendTranslationToLiltConnectorHandler; + $this->settingsRepository = $settingsRepository; + } + + /** * @throws Throwable * @throws ElementNotFoundException @@ -37,104 +94,75 @@ class SendJobToLiltConnectorHandler */ public function __invoke(Job $job): void { - $jobLilt = Craftliltplugin::getInstance()->connectorJobRepository->create( + $isQueueEachTranslationFileSeparately = $this->settingsRepository->isQueueEachTranslationFileSeparately(); + + $jobLilt = $this->connectorJobRepository->create( $job->title, strtoupper($job->translationWorkflow) ); - Craftliltplugin::getInstance()->jobLogsRepository->create( + $this->jobLogsRepository->create( $job->id, Craft::$app->getUser()->getId(), sprintf('Lilt job created (id: %d)', $jobLilt->getId()) ); - $elementIdsToTranslate = $job->getElementIds(); - $translations = Craftliltplugin::getInstance()->translationRepository->findRecordsByJobId($job->id); - /** - * @var TranslationRecord[][] $translationsMapped - */ - $translationsMapped = []; - - foreach ($translations as $translation) { - $translationsMapped[$translation->versionId][$translation->targetSiteId] = $translation; - } + $translationsMapped = $this->getTranslationsMapped($job); - foreach ($elementIdsToTranslate as $elementId) { + foreach ($job->getElementIds() as $elementId) { $versionId = $job->getElementVersionId($elementId); $element = Craft::$app->elements->getElementById($versionId, null, $job->sourceSiteId); if (!$element) { - //TODO: handle + Craft::error( + sprintf("Can't find element: %d for job: %d", $versionId, $job->id) + ); + continue; } foreach ($job->getTargetSiteIds() as $targetSiteId) { - //Create draft with & update all values to source element - $draft = Craftliltplugin::getInstance()->createDraftHandler->create( - new CreateDraftCommand( - $element, - $job->title, - $job->sourceSiteId, - $targetSiteId, - $job->translationWorkflow, - $job->authorId - ) - ); - - $content = Craftliltplugin::getInstance()->elementTranslatableContentProvider->provide( - $draft - ); - - $slug = !empty($element->slug) ? $element->slug : 'no-slug-available'; - - $result = $this->createJobFile( - $content, - $versionId, - $jobLilt->getId(), - Craftliltplugin::getInstance()->languageMapper->getLanguageBySiteId((int)$job->sourceSiteId), - Craftliltplugin::getInstance()->languageMapper->getLanguagesBySiteIds( - [$targetSiteId] - ), - null, //TODO: $job->dueDate is not in use - $slug - ); - - if (!$result) { - $this->updateJob($job, $jobLilt->getId(), Job::STATUS_FAILED); + $translation = $translationsMapped[$versionId][$targetSiteId] ?? null; + if ($isQueueEachTranslationFileSeparately) { + Queue::push( + new SendTranslationToConnector([ + 'jobId' => $job->id, + 'translationId' => $translation->id ?? null, + 'elementId' => $elementId, + 'versionId' => $versionId, + 'targetSiteId' => $targetSiteId, + ]), + SendTranslationToConnector::PRIORITY, + SendTranslationToConnector::getDelay(), + SendTranslationToConnector::TTR + ); - throw new \RuntimeException('Translations not created, upload failed'); + continue; } - $translation = $translationsMapped[$versionId][$targetSiteId] ?? null; - if ($translation === null) { - $translation = Craftliltplugin::getInstance()->translationRepository->create( - $job->id, + $this->sendTranslationToLiltConnectorHandler->send( + new SendTranslationCommand( $elementId, $versionId, - $job->sourceSiteId, $targetSiteId, - TranslationRecord::STATUS_IN_PROGRESS - ); - } - - $translation->sourceContent = $content; - $translation->translatedDraftId = $draft->id; - $translation->markAttributeDirty('sourceContent'); - $translation->markAttributeDirty('translatedDraftId'); - - if (!$translation->save()) { - $this->updateJob($job, $jobLilt->getId(), Job::STATUS_FAILED); - - throw new \RuntimeException('Translations not created, upload failed'); - } + $element, + $jobLilt->getId(), + $job, + $translation + ) + ); } } $this->updateJob($job, $jobLilt->getId(), Job::STATUS_IN_PROGRESS); - Craftliltplugin::getInstance()->connectorJobRepository->start($jobLilt->getId()); + if ($isQueueEachTranslationFileSeparately) { + return; + } + + $this->connectorJobRepository->start($jobLilt->getId()); - Craftliltplugin::getInstance()->jobLogsRepository->create( + $this->jobLogsRepository->create( $job->id, Craft::$app->getUser()->getId(), 'Job uploaded to Lilt Platform' @@ -150,49 +178,19 @@ public function __invoke(Job $job): void ); } - private function createJobFile( - array $content, - int $entryId, - int $jobId, - string $sourceLanguage, - array $targetSiteLanguages, - ?DateTimeInterface $dueDate, - string $slug - ): bool { - $contentString = json_encode($content); - - if (!empty($slug)) { - $slug = substr($slug, 0, 150); - } - - return Craftliltplugin::getInstance()->connectorJobsFileRepository->addFileToJob( - $jobId, - 'element_' . $entryId . '_' . $slug . '.json+html', - $contentString, - $sourceLanguage, - $targetSiteLanguages, - $dueDate - ); - } - /** * @param Job $job * @param int $jobLiltId * @param string $status + * * @return void - * @throws ElementNotFoundException + * * @throws Exception * @throws StaleObjectException * @throws Throwable */ private function updateJob(Job $job, int $jobLiltId, string $status): void { - $job->liltJobId = $jobLiltId; - $job->status = $status; - - //TODO: check how it works - Craft::$app->getElements()->saveElement($job, true, true, true); - $jobRecord = JobRecord::findOne(['id' => $job->id]); $jobRecord->status = $status; @@ -201,4 +199,22 @@ private function updateJob(Job $job, int $jobLiltId, string $status): void $jobRecord->update(); Craft::$app->getCache()->flush(); } + + /** + * @param Job $job + * @return array|TranslationRecord[][] + */ + private function getTranslationsMapped(Job $job): array + { + $translations = $this->translationRepository->findRecordsByJobId($job->id); + /** + * @var TranslationRecord[][] $translationsMapped + */ + $translationsMapped = []; + + foreach ($translations as $translation) { + $translationsMapped[$translation->versionId][$translation->targetSiteId] = $translation; + } + return $translationsMapped; + } } diff --git a/src/services/handlers/SendTranslationToLiltConnectorHandler.php b/src/services/handlers/SendTranslationToLiltConnectorHandler.php new file mode 100644 index 00000000..e5340c51 --- /dev/null +++ b/src/services/handlers/SendTranslationToLiltConnectorHandler.php @@ -0,0 +1,213 @@ +jobLogsRepository = $jobLogsRepository; + $this->translationRepository = $translationRepository; + $this->connectorJobsFileRepository = $connectorJobsFileRepository; + $this->createDraftHandler = $createDraftHandler; + $this->elementTranslatableContentProvider = $elementTranslatableContentProvider; + $this->languageMapper = $languageMapper; + } + + + /** + * @throws Throwable + * @throws ElementNotFoundException + * @throws ApiException + * @throws Exception + * @throws StaleObjectException + */ + public function send(SendTranslationCommand $sendTranslationCommand): void + { + $job = $sendTranslationCommand->getJob(); + $element = $sendTranslationCommand->getElement(); + $versionId = $sendTranslationCommand->getVersionId(); + $elementId = $sendTranslationCommand->getElementId(); + $liltJobId = $sendTranslationCommand->getLiltJobId(); + $translation = $sendTranslationCommand->getTranslationRecord(); + $targetSiteId = $sendTranslationCommand->getTargetSiteId(); + + //Create draft with & update all values to source element + $draft = $this->createDraftHandler->create( + new CreateDraftCommand( + $element, + $job->title, + $job->sourceSiteId, + $targetSiteId, + $job->translationWorkflow, + $job->authorId + ) + ); + + $content = $this->elementTranslatableContentProvider->provide( + $draft + ); + + $slug = !empty($element->slug) ? $element->slug : 'no-slug-available'; + + $result = $this->createJobFile( + $content, + $versionId, + $liltJobId, + $this->languageMapper->getLanguageBySiteId((int)$job->sourceSiteId), + $this->languageMapper->getLanguagesBySiteIds( + [$targetSiteId] + ), + null, //TODO: $job->dueDate is not in use + $slug + ); + + if (!$result) { + $this->updateJob($job, $liltJobId, Job::STATUS_FAILED); + + throw new \RuntimeException('Translations not created, upload failed'); + } + + if ($translation === null) { + $translation = $this->translationRepository->create( + $job->id, + $elementId, + $versionId, + $job->sourceSiteId, + $targetSiteId, + TranslationRecord::STATUS_IN_PROGRESS + ); + } + + $translation->sourceContent = $content; + $translation->translatedDraftId = $draft->id; + $translation->markAttributeDirty('sourceContent'); + $translation->markAttributeDirty('translatedDraftId'); + + if (!$translation->save()) { + $this->updateJob($job, $liltJobId, Job::STATUS_FAILED); + + throw new \RuntimeException('Translations not created, upload failed'); + } + } + + + private function createJobFile( + array $content, + int $entryId, + int $jobId, + string $sourceLanguage, + array $targetSiteLanguages, + ?DateTimeInterface $dueDate, + string $slug + ): bool { + $contentString = json_encode($content); + + if (!empty($slug)) { + $slug = substr($slug, 0, 150); + } + + return $this->connectorJobsFileRepository->addFileToJob( + $jobId, + 'element_' . $entryId . '_' . $slug . '.json+html', + $contentString, + $sourceLanguage, + $targetSiteLanguages, + $dueDate + ); + } + + /** + * @param Job $job + * @param int $jobLiltId + * @param string $status + * + * @return void + * + * @throws Exception + * @throws StaleObjectException + * @throws Throwable + */ + private function updateJob(Job $job, int $jobLiltId, string $status): void + { + $jobRecord = JobRecord::findOne(['id' => $job->id]); + + $jobRecord->status = $status; + $jobRecord->liltJobId = $jobLiltId; + + $jobRecord->update(); + Craft::$app->getCache()->flush(); + } +} diff --git a/src/services/handlers/commands/SendTranslationCommand.php b/src/services/handlers/commands/SendTranslationCommand.php new file mode 100644 index 00000000..2dc89e5b --- /dev/null +++ b/src/services/handlers/commands/SendTranslationCommand.php @@ -0,0 +1,131 @@ +elementId = $elementId; + $this->versionId = $versionId; + $this->targetSiteId = $targetSiteId; + $this->element = $element; + $this->liltJobId = $liltJobId; + $this->job = $job; + $this->translationRecord = $translationRecord; + } + + /** + * @return int + */ + public function getElementId(): int + { + return $this->elementId; + } + + /** + * @return int + */ + public function getVersionId(): int + { + return $this->versionId; + } + + /** + * @return int + */ + public function getTargetSiteId(): int + { + return $this->targetSiteId; + } + + /** + * @return ElementInterface + */ + public function getElement(): ElementInterface + { + return $this->element; + } + + /** + * @return int + */ + public function getLiltJobId(): int + { + return $this->liltJobId; + } + + /** + * @return Job + */ + public function getJob(): Job + { + return $this->job; + } + + /** + * @return TranslationRecord|null + */ + public function getTranslationRecord(): ?TranslationRecord + { + return $this->translationRecord; + } +} diff --git a/src/services/listeners/AfterErrorListener.php b/src/services/listeners/AfterErrorListener.php index f5fb82e0..95de8025 100644 --- a/src/services/listeners/AfterErrorListener.php +++ b/src/services/listeners/AfterErrorListener.php @@ -18,6 +18,7 @@ use lilthq\craftliltplugin\modules\FetchTranslationFromConnector; use lilthq\craftliltplugin\modules\FetchVerifiedJobTranslationsFromConnector; use lilthq\craftliltplugin\modules\SendJobToConnector; +use lilthq\craftliltplugin\modules\SendTranslationToConnector; use lilthq\craftliltplugin\records\JobRecord; use lilthq\craftliltplugin\records\TranslationRecord; use yii\base\Event; @@ -31,6 +32,7 @@ class AfterErrorListener implements ListenerInterface FetchVerifiedJobTranslationsFromConnector::class, FetchTranslationFromConnector::class, SendJobToConnector::class, + SendTranslationToConnector::class, ]; public function register(): void diff --git a/src/services/repositories/SettingsRepository.php b/src/services/repositories/SettingsRepository.php index da67aa6d..813f77d6 100644 --- a/src/services/repositories/SettingsRepository.php +++ b/src/services/repositories/SettingsRepository.php @@ -16,6 +16,8 @@ class SettingsRepository public const ENABLE_ENTRIES_FOR_TARGET_SITES = 'enable_entries_for_target_sites'; public const COPY_ENTRIES_SLUG_FROM_SOURCE_TO_TARGET = 'copy_entries_slug_from_source_to_target'; + public const QUEUE_EACH_TRANSLATION_FILE_SEPARATELY = 'queue_each_translation_file_separately'; + public function saveLiltApiConnectionConfiguration(string $connectorApiUrl, string $connectorApiKey): void { # connectorApiKey @@ -36,14 +38,27 @@ public function saveLiltApiConnectionConfiguration(string $connectorApiUrl, stri $connectorApiUrlRecord->save(); } + public function isQueueEachTranslationFileSeparately(): bool + { + $settingValue = SettingRecord::findOne( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + + if (empty($settingValue) || empty($settingValue->value)) { + return false; + } + + return (bool)$settingValue->value; + } + public function save(string $name, string $value): bool { - $enableEntriesForTargetSites = SettingRecord::findOne(['name' => $name]); - if (!$enableEntriesForTargetSites) { - $enableEntriesForTargetSites = new SettingRecord(['name' => $name]); + $settingRecord = SettingRecord::findOne(['name' => $name]); + if (!$settingRecord) { + $settingRecord = new SettingRecord(['name' => $name]); } - $enableEntriesForTargetSites->value = $value; - return $enableEntriesForTargetSites->save(); + $settingRecord->value = $value; + return $settingRecord->save(); } } diff --git a/src/templates/_components/utilities/configuration.twig b/src/templates/_components/utilities/configuration.twig index 775a308a..cb0eb25f 100644 --- a/src/templates/_components/utilities/configuration.twig +++ b/src/templates/_components/utilities/configuration.twig @@ -80,6 +80,17 @@ }) }} +
+ {{ forms.checkbox({ + label: 'Queue each translation file separately', + name: 'queueEachTranslationFileSeparately', + id: 'queueEachTranslationFileSeparately', + checked: queueEachTranslationFileSeparately, + errors: model is defined? model.getErrors('queueEachTranslationFileSeparately') : [], + disabled: liltConfigDisabled + }) }} +
+ {{ forms.textField({ name: 'liltConfigDisabled', id: 'liltConfigDisabled', diff --git a/src/utilities/Configuration.php b/src/utilities/Configuration.php index cd17aa16..2c5e3e41 100644 --- a/src/utilities/Configuration.php +++ b/src/utilities/Configuration.php @@ -11,6 +11,7 @@ use LiltConnectorSDK\Model\SettingsResponse; use lilthq\craftliltplugin\Craftliltplugin; use lilthq\craftliltplugin\records\SettingRecord; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; class Configuration extends Utility { @@ -86,6 +87,11 @@ public static function contentHtml(): string ); $copyEntriesSlugFromSourceToTarget = (bool) ($copyEntriesSlugFromSourceToTargetRecord->value ?? false); + $queueEachTranslationFileSeparately = SettingRecord::findOne( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY,] + ); + $queueEachTranslationFileSeparately = (bool) ($queueEachTranslationFileSeparately->value ?? false); + return Craft::$app->getView()->renderTemplate( 'craft-lilt-plugin/_components/utilities/configuration.twig', [ @@ -99,6 +105,7 @@ public static function contentHtml(): string 'liltConfigDisabled' => (int) $liltConfigDisabled, 'enableEntriesForTargetSites' => $enableEntriesForTargetSites, 'copyEntriesSlugFromSourceToTarget' => $copyEntriesSlugFromSourceToTarget, + 'queueEachTranslationFileSeparately' => $queueEachTranslationFileSeparately, ] ); } diff --git a/tests/_support/Helper/CraftLiltPluginHelper.php b/tests/_support/Helper/CraftLiltPluginHelper.php index 4b5fb2a5..0164eb5d 100644 --- a/tests/_support/Helper/CraftLiltPluginHelper.php +++ b/tests/_support/Helper/CraftLiltPluginHelper.php @@ -18,9 +18,11 @@ use lilthq\craftliltplugin\elements\Job; use lilthq\craftliltplugin\records\I18NRecord; use lilthq\craftliltplugin\records\JobRecord; +use lilthq\craftliltplugin\records\SettingRecord; use lilthq\craftliltplugin\records\TranslationRecord; use lilthq\craftliltplugin\services\handlers\commands\CreateDraftCommand; use lilthq\craftliltplugin\services\handlers\commands\CreateJobCommand; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; use yii\base\InvalidArgumentException; class CraftLiltPluginHelper extends Module @@ -176,6 +178,21 @@ public function assertTranslationsContentMatch(array $translations, array $expec } } + public function setQueueEachTranslationFileSeparately(int $value): void + { + $settingRecord = SettingRecord::findOne( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + if (!$settingRecord) { + $settingRecord = new SettingRecord( + ['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY] + ); + } + + $settingRecord->value = $value; + $settingRecord->save(); + } + public function assertI18NRecordsExist(int $targetSiteId, array $expectedTranslations): void { $actualRecords = Craftliltplugin::getInstance()->i18NRepository->findAllByTargetSiteId($targetSiteId); diff --git a/tests/integration/controllers/PostConfigurationControllerCest.php b/tests/integration/controllers/PostConfigurationControllerCest.php index 6feb7604..744c8bfb 100644 --- a/tests/integration/controllers/PostConfigurationControllerCest.php +++ b/tests/integration/controllers/PostConfigurationControllerCest.php @@ -11,6 +11,7 @@ use LiltConnectorSDK\Model\SettingsResponse; use lilthq\craftliltplugin\parameters\CraftliltpluginParameters; use lilthq\craftliltplugin\records\SettingRecord; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; use lilthq\craftliltplugintests\integration\AbstractIntegrationCest; use PHPUnit\Framework\Assert; @@ -55,14 +56,17 @@ public function testSuccess(IntegrationTester $I): void 'liltTranslationWorkflow' => SettingsResponse::LILT_TRANSLATION_WORKFLOW_INSTANT, 'enableEntriesForTargetSites' => true, 'copyEntriesSlugFromSourceToTarget' => true, + 'queueEachTranslationFileSeparately' => false, ] ); $connectorApiKeyRecord = SettingRecord::findOne(['name' => 'connector_api_key']); $connectorApiUrlRecord = SettingRecord::findOne(['name' => 'connector_api_url']); + $queueEachTranslationFileSeparately = SettingRecord::findOne(['name' => SettingsRepository::QUEUE_EACH_TRANSLATION_FILE_SEPARATELY]); Assert::assertSame('this-is-connector-api-key', $connectorApiKeyRecord->value); Assert::assertSame('http://wiremock/api/v1.0/this-is-connector-api-url', $connectorApiUrlRecord->value); + Assert::assertSame('0', $queueEachTranslationFileSeparately->value); } public function _after(IntegrationTester $I): void diff --git a/tests/integration/controllers/job/PostJobRetryControllerCest.php b/tests/integration/controllers/job/PostJobRetryControllerCest.php index 04afc3a2..a017c062 100644 --- a/tests/integration/controllers/job/PostJobRetryControllerCest.php +++ b/tests/integration/controllers/job/PostJobRetryControllerCest.php @@ -45,6 +45,8 @@ public function _fixtures(): array */ public function testRetrySuccess(IntegrationTester $I): void { + $I->setQueueEachTranslationFileSeparately(0); + $I->amLoggedInAs( Craft::$app->getUsers()->getUserById(1) ); diff --git a/tests/integration/modules/SendJobToConnectorCest.php b/tests/integration/modules/SendJobToConnectorCest.php index 7fe0acb7..bbc24932 100644 --- a/tests/integration/modules/SendJobToConnectorCest.php +++ b/tests/integration/modules/SendJobToConnectorCest.php @@ -24,7 +24,9 @@ use lilthq\craftliltplugin\modules\FetchJobStatusFromConnector; use lilthq\craftliltplugin\modules\SendJobToConnector; use lilthq\craftliltplugin\parameters\CraftliltpluginParameters; +use lilthq\craftliltplugin\records\SettingRecord; use lilthq\craftliltplugin\records\TranslationRecord; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; use lilthq\craftliltplugintests\integration\AbstractIntegrationCest; use lilthq\tests\fixtures\EntriesFixture; use lilthq\tests\fixtures\ExpectedElementContent; @@ -58,6 +60,8 @@ private function getController(): PostCreateJobController */ public function testCreateJobSuccess(IntegrationTester $I): void { + $I->setQueueEachTranslationFileSeparately(0); + $user = Craft::$app->getUsers()->getUserById(1); $I->amLoggedInAs($user); @@ -180,6 +184,8 @@ public function testCreateJobSuccess(IntegrationTester $I): void public function testSendCopySourceFlow(IntegrationTester $I): void { + $I->setQueueEachTranslationFileSeparately(0); + $user = Craft::$app->getUsers()->getUserById(1); $I->amLoggedInAs($user); @@ -294,8 +300,10 @@ public function testSendCopySourceFlow(IntegrationTester $I): void /** * @throws ModuleException */ - public function testCreateJobWithUnexpectedStatusFromConnector(IntegrationTester $I, $scenario): void + public function testCreateJobWithUnexpectedStatusFromConnector(IntegrationTester $I): void { + $I->setQueueEachTranslationFileSeparately(0); + $element = Entry::find() ->where(['authorId' => 1]) ->orderBy(['id' => SORT_DESC])