From 15304e49b81245ca53fcc628872a888a4e5dfb63 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 22 Apr 2024 16:36:09 +0200 Subject: [PATCH 1/7] feat: Allow duplication --- src/reference/openapi.yml | 260 ++++++----- src/routes/dossiers/_post/duplication.spec.ts | 434 ++++++++++++++++++ src/routes/dossiers/_post/index.ts | 304 ++++++++++++ src/schema.ts | 92 ++-- 4 files changed, 937 insertions(+), 153 deletions(-) create mode 100644 src/routes/dossiers/_post/duplication.spec.ts diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 6e3b039aa..2b5efa6c0 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9688,10 +9688,39 @@ paths: Example 1: value: id: 1 - requestBody: - $ref: '#/components/requestBodies/DossierData' security: - JWT: [] + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/DossierCreationData' + - type: object + x-stoplight: + id: 7ewqh8piy4dc8 + properties: + duplicate: + type: object + x-stoplight: + id: qjlhi5gbkesdj + properties: + fields: + type: integer + x-stoplight: + id: eynp882a0yi85 + useCases: + type: integer + x-stoplight: + id: aeis659jristy + mailMerges: + type: integer + x-stoplight: + id: uzgqkctl150pz + pages: + type: integer + x-stoplight: + id: qhyggxgrgajc1 '/dossiers/{campaign}': parameters: - name: campaign @@ -9712,10 +9741,13 @@ paths: type: object properties: {} description: '' - requestBody: - $ref: '#/components/requestBodies/DossierData' security: - JWT: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DossierCreationData' get: summary: '' operationId: get-dossiers-campaign @@ -10955,6 +10987,117 @@ components: pattern: '^[A-Z][A-Z]$' minLength: 2 maxLength: 2 + DossierCreationData: + type: object + x-examples: + Example 1: + project: 0 + testType: 0 + title: + customer: string + tester: string + startDate: '2019-08-24T14:15:22Z' + endDate: '2019-08-24T14:15:22Z' + closeDate: '2019-08-24T14:15:22Z' + deviceList: + - 0 + csm: 0 + roles: + - role: 0 + user: 0 + description: string + productLink: string + goal: string + outOfScope: string + deviceRequirements: string + target: + notes: string + size: 0 + countries: + - st + languages: + - 0 + browsers: + - 0 + productType: 0 + properties: + project: + type: integer + testType: + type: integer + title: + type: object + required: + - customer + properties: + customer: + type: string + tester: + type: string + startDate: + type: string + format: date-time + endDate: + type: string + format: date-time + closeDate: + type: string + format: date-time + deviceList: + type: array + items: + type: integer + csm: + type: integer + roles: + type: array + items: + type: object + properties: + role: + type: integer + user: + type: integer + required: + - role + - user + description: + type: string + productLink: + type: string + goal: + type: string + outOfScope: + type: string + deviceRequirements: + type: string + target: + type: object + properties: + notes: + type: string + size: + type: integer + countries: + type: array + items: + $ref: '#/components/schemas/CountryCode' + languages: + type: array + items: + type: integer + browsers: + type: array + items: + type: integer + productType: + type: integer + required: + - project + - testType + - title + - startDate + - deviceList securitySchemes: JWT: type: http @@ -11253,114 +11396,7 @@ components: schema: type: string examples: {} - requestBodies: - DossierData: - content: - application/json: - schema: - type: object - x-examples: - Example 1: - project: 1 - testType: 1 - title: - customer: Campaign Title for Customer - tester: Campaign Title for Tester - startDate: '2019-08-24T14:15:22Z' - endDate: '2019-08-24T14:15:22Z' - deviceList: - - 1 - - 5 - - 10 - - 36 - properties: - project: - type: integer - testType: - type: integer - title: - type: object - required: - - customer - properties: - customer: - type: string - tester: - type: string - startDate: - type: string - format: date-time - endDate: - type: string - format: date-time - closeDate: - type: string - format: date-time - deviceList: - type: array - items: - type: integer - csm: - type: number - roles: - type: array - items: - type: object - properties: - role: - type: number - user: - type: number - required: - - role - - user - description: - type: string - productLink: - type: string - goal: - type: string - outOfScope: - type: string - deviceRequirements: - type: string - target: - type: object - properties: - notes: - type: string - size: - type: integer - countries: - type: array - items: - $ref: '#/components/schemas/CountryCode' - languages: - type: array - x-stoplight: - id: dy0yofmqy4ayn - items: - x-stoplight: - id: bko78gf0sdhjz - type: integer - browsers: - type: array - x-stoplight: - id: jc5nfd988spfc - items: - x-stoplight: - id: 9wdgspthmxvhf - type: integer - productType: - type: integer - x-stoplight: - id: ilm9662wwxef3 - required: - - project - - testType - - title - - startDate - - deviceList + requestBodies: {} tags: - name: Authentication - name: Campaign diff --git a/src/routes/dossiers/_post/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts new file mode 100644 index 000000000..673066630 --- /dev/null +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -0,0 +1,434 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const baseRequest = { + project: 1, + testType: 1, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers - duplication", () => { + beforeAll(async () => { + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + ]); + + await tryber.tables.WpOptions.do().insert({ + option_name: "polylang", + option_value: + 'a:16:{s:7:"browser";b:1;s:7:"rewrite";i:1;s:12:"hide_default";i:1;s:10:"force_lang";i:1;s:13:"redirect_lang";i:1;s:13:"media_support";i:1;s:9:"uninstall";i:0;s:4:"sync";a:0:{}s:10:"post_types";a:2:{i:0;s:6:"manual";i:1;s:7:"preview";}s:10:"taxonomies";a:0:{}s:7:"domains";a:0:{}s:7:"version";s:5:"3.1.1";s:16:"first_activation";i:1562051895;s:12:"default_lang";s:2:"en";s:9:"nav_menus";a:1:{s:15:"crowdappquality";a:1:{s:7:"primary";a:2:{s:2:"en";i:2;s:2:"it";i:163;}}}s:16:"previous_version";s:3:"3.1";}', + }); + }); + + beforeEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + platform_id: 1, + start_date: "1970-01-01", + end_date: "1970-01-01", + title: "Origin Campaign", + customer_title: "Origin Campaign", + page_manual_id: 1, + page_preview_id: 2, + customer_id: 1, + project_id: 1, + pm_id: 1, + }); + + await tryber.tables.WpAppqCampaignAdditionalFields.do().insert({ + cp_id: 1, + slug: "field", + title: "Field", + type: "regex", + validation: ".*", + error_message: "Invalid format", + }); + + await tryber.tables.WpAppqCampaignTask.do().insert({ + id: 1, + campaign_id: 1, + title: "Task", + content: "Task content", + simple_title: "Task", + info: "Task info", + prefix: "T", + language: "en", + optimize_media: 1, + is_required: 1, + jf_code: "T", + jf_text: "JF", + }); + + await tryber.tables.WpAppqCampaignTaskGroup.do().insert({ + task_id: 1, + group_id: 4, + }); + + await tryber.tables.WpAppqCronJobs.do().insert({ + id: 1, + campaign_id: 1, + display_name: "Test Mail Merge", + email_template_id: 1, + template_text: "Test Template", + template_json: "{}", + last_editor_id: 1, + creation_date: "1970-01-01", + template_id: 1, + update_date: "1970-01-01", + executed_on: "", + }); + + const page = { + post_content: "Test Content", + post_status: "publish", + post_author: 1, + post_excerpt: "Test Excerpt", + to_ping: "", + pinged: "", + post_content_filtered: "", + }; + + await tryber.tables.WpPosts.do().insert([ + { + ...page, + ID: 1, + post_title: "CP1 - Test Manual", + post_type: "manual", + }, + { + ...page, + ID: 2, + post_title: "CP1 - Test Preview", + post_type: "preview", + }, + { + ...page, + ID: 3, + post_title: "CP1 - Test Manual (it)", + post_type: "manual", + }, + { + ...page, + ID: 4, + post_title: "CP1 - Test Preview (it)", + post_type: "preview", + }, + ]); + + await tryber.tables.WpTermRelationships.do().insert([ + { object_id: 1, term_taxonomy_id: 1 }, + { object_id: 2, term_taxonomy_id: 2 }, + ]); + + await tryber.tables.WpTermTaxonomy.do().insert([ + { + term_taxonomy_id: 1, + term_id: 1, + taxonomy: "post_translations", + description: 'a:2:{s:2:"en";i:1;s:2:"it";i:3;}', + }, + { + term_taxonomy_id: 2, + term_id: 2, + taxonomy: "post_translations", + description: 'a:2:{s:2:"en";i:2;s:2:"it";i:4;}', + }, + ]); + + await tryber.tables.WpPostmeta.do().insert([ + { + post_id: 1, + meta_key: "_wp_page_template", + meta_value: "page-manual.php", + }, + { post_id: 1, meta_key: "acf_field", meta_value: "value" }, + { + post_id: 2, + meta_key: "_wp_page_template", + meta_value: "page-preview.php", + }, + { post_id: 2, meta_key: "acf_field", meta_value: "value" }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.WpAppqCampaignAdditionalFields.do().delete(); + await tryber.tables.WpAppqCampaignTask.do().delete(); + await tryber.tables.WpAppqCampaignTaskGroup.do().delete(); + await tryber.tables.WpAppqCronJobs.do().delete(); + await tryber.tables.WpPosts.do().delete(); + await tryber.tables.WpPostmeta.do().delete(); + await tryber.tables.WpTermRelationships.do().delete(); + await tryber.tables.WpTermTaxonomy.do().delete(); + }); + + it("Should return 400 if campaign to duplicate does not exist", async () => { + const fields = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { fields: 100 } }); + + expect(fields.status).toBe(400); + + const useCases = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { useCases: 100 } }); + + expect(useCases.status).toBe(400); + + const mailMerges = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { mailMerges: 100 } }); + + expect(mailMerges.status).toBe(400); + + const pages = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { pages: 100 } }); + + expect(pages.status).toBe(400); + }); + + it("Should duplicate additional fields", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { fields: 1 } }); + expect(response.status).toBe(201); + + const id = response.body.id; + + const fields = await tryber.tables.WpAppqCampaignAdditionalFields.do() + .select() + .where({ cp_id: id }); + + expect(fields).toHaveLength(1); + expect(fields[0]).toMatchObject({ + slug: "field", + title: "Field", + type: "regex", + validation: ".*", + error_message: "Invalid format", + }); + }); + + it("Should duplicate usecases", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { useCases: 1 } }); + expect(response.status).toBe(201); + + const id = response.body.id; + + const tasks = await tryber.tables.WpAppqCampaignTask.do() + .select() + .where({ campaign_id: id }); + + expect(tasks).toHaveLength(1); + expect(tasks[0]).toMatchObject({ + title: "Task", + content: "Task content", + simple_title: "Task", + info: "Task info", + prefix: "T", + language: "en", + optimize_media: 1, + is_required: 1, + jf_code: "T", + jf_text: "JF", + }); + + const groups = await tryber.tables.WpAppqCampaignTaskGroup.do() + .select() + .where({ task_id: tasks[0].id }); + + expect(groups).toHaveLength(1); + expect(groups[0]).toMatchObject({ group_id: 4 }); + }); + + it("Should duplicate mail merges", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { mailMerges: 1 } }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const mailMerges = await tryber.tables.WpAppqCronJobs.do() + .select() + .where({ campaign_id: id }); + + expect(mailMerges).toHaveLength(1); + expect(mailMerges[0]).toMatchObject({ + display_name: "Test Mail Merge", + email_template_id: 1, + template_text: "Test Template", + template_json: "{}", + last_editor_id: 1, + }); + }); + + it("Should duplicate pages", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + + .send({ ...baseRequest, duplicate: { pages: 1 } }); + + const posts = await tryber.tables.WpPosts.do().select(); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const pages = await tryber.tables.WpAppqEvdCampaign.do() + .select("page_manual_id", "page_preview_id") + .where({ id }); + + expect(pages).toHaveLength(1); + + const manualId = pages[0].page_manual_id; + const previewId = pages[0].page_preview_id; + + const manual = await tryber.tables.WpPosts.do() + .select() + .where({ ID: manualId }); + + expect(manual).toHaveLength(1); + expect(manual[0]).toMatchObject({ + post_title: `CP${id} - Test Manual`, + post_type: "manual", + post_content: "Test Content", + post_status: "publish", + post_author: 1, + post_excerpt: "Test Excerpt", + }); + + const preview = await tryber.tables.WpPosts.do() + .select() + .where({ ID: previewId }); + + expect(preview).toHaveLength(1); + + expect(preview[0]).toMatchObject({ + post_title: `CP${id} - Test Preview`, + post_type: "preview", + post_content: "Test Content", + post_status: "publish", + post_author: 1, + post_excerpt: "Test Excerpt", + }); + + const manualMeta = await tryber.tables.WpPostmeta.do() + .select() + .where({ post_id: manualId }); + + expect(manualMeta).toHaveLength(2); + expect(manualMeta[0]).toMatchObject({ + meta_key: "_wp_page_template", + meta_value: "page-manual.php", + }); + + expect(manualMeta[1]).toMatchObject({ + meta_key: "acf_field", + meta_value: "value", + }); + + const previewMeta = await tryber.tables.WpPostmeta.do() + .select() + .where({ post_id: previewId }); + + expect(previewMeta).toHaveLength(2); + expect(previewMeta[0]).toMatchObject({ + meta_key: "_wp_page_template", + meta_value: "page-preview.php", + }); + + expect(previewMeta[1]).toMatchObject({ + meta_key: "acf_field", + meta_value: "value", + }); + }); + + it("Should duplicate all languages", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + + .send({ ...baseRequest, duplicate: { pages: 1 } }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const posts = await tryber.tables.WpPosts.do().select(); + + expect(posts).toHaveLength(8); + + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Manual`, + post_type: "manual", + }) + ); + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Preview`, + post_type: "preview", + }) + ); + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Manual (it)`, + post_type: "manual", + }) + ); + expect(posts).toContainEqual( + expect.objectContaining({ + post_title: `CP${id} - Test Preview (it)`, + post_type: "preview", + }) + ); + }); +}); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index fe73a9164..560aae2f0 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -3,11 +3,33 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; import AdminRoute from "@src/features/routes/AdminRoute"; +import { unserialize } from "php-unserialize"; export default class RouteItem extends AdminRoute<{ response: StoplightOperations["post-dossiers"]["responses"]["201"]["content"]["application/json"]; body: StoplightOperations["post-dossiers"]["requestBody"]["content"]["application/json"]; }> { + private duplicate: { + fieldsFrom?: number; + useCasesFrom?: number; + mailMergesFrom?: number; + pagesFrom?: number; + } = {}; + + constructor(config: RouteClassConfiguration) { + super(config); + + const { duplicate } = this.getBody(); + + if (duplicate) { + if (duplicate.fields) this.duplicate.fieldsFrom = duplicate.fields; + if (duplicate.useCases) this.duplicate.useCasesFrom = duplicate.useCases; + if (duplicate.mailMerges) + this.duplicate.mailMergesFrom = duplicate.mailMerges; + if (duplicate.pages) this.duplicate.pagesFrom = duplicate.pages; + } + } + protected async filter() { if (!(await super.filter())) return false; if (await this.invalidRolesSubmitted()) { @@ -26,10 +48,32 @@ export default class RouteItem extends AdminRoute<{ this.setError(400, new OpenapiError("Invalid devices")); return false; } + if (await this.campaignToDuplicateDoesNotExist()) { + this.setError(400, new OpenapiError("Invalid campaign to duplicate")); + return false; + } return true; } + private async campaignToDuplicateDoesNotExist() { + const ids = [ + ...new Set([ + ...(this.duplicate.fieldsFrom ? [this.duplicate.fieldsFrom] : []), + ...(this.duplicate.useCasesFrom ? [this.duplicate.useCasesFrom] : []), + ...(this.duplicate.mailMergesFrom + ? [this.duplicate.mailMergesFrom] + : []), + ...(this.duplicate.pagesFrom ? [this.duplicate.pagesFrom] : []), + ]), + ]; + const campaigns = await tryber.tables.WpAppqEvdCampaign.do() + .select("id") + .whereIn("id", ids); + + return campaigns.length !== ids.length; + } + private async invalidRolesSubmitted() { const { roles } = this.getBody(); if (!roles) return false; @@ -82,6 +126,8 @@ export default class RouteItem extends AdminRoute<{ const campaignId = await this.createCampaign(); await this.linkRolesToCampaign(campaignId); + await this.copyDataFromDuplication(campaignId); + this.setSuccess(201, { id: campaignId, }); @@ -184,6 +230,264 @@ export default class RouteItem extends AdminRoute<{ await this.assignOlps(campaignId); } + private async copyDataFromDuplication(campaignId: number) { + await this.duplicateFields(campaignId); + await this.duplicateUsecases(campaignId); + await this.duplicateMailMerge(campaignId); + await this.duplicatePages(campaignId); + } + + private async duplicateFields(campaignId: number) { + if (!this.duplicate.fieldsFrom) return; + + const fields = await tryber.tables.WpAppqCampaignAdditionalFields.do() + .select() + .where({ + cp_id: this.duplicate.fieldsFrom, + }); + + await tryber.tables.WpAppqCampaignAdditionalFields.do().insert( + fields.map((field) => { + const { id, ...rest } = field; + return { + ...rest, + cp_id: campaignId, + }; + }) + ); + } + + private async duplicateUsecases(campaignId: number) { + if (!this.duplicate.useCasesFrom) return; + + const tasks = await tryber.tables.WpAppqCampaignTask.do().select().where({ + campaign_id: this.duplicate.useCasesFrom, + }); + + const taskMap = new Map(); + + for (const task of tasks) { + const { id, ...rest } = task; + const newItem = await tryber.tables.WpAppqCampaignTask.do() + .insert({ + ...rest, + campaign_id: campaignId, + }) + .returning("id"); + taskMap.set(id, newItem[0].id ?? newItem[0]); + } + + const groups = await tryber.tables.WpAppqCampaignTaskGroup.do() + .select() + .whereIn( + "task_id", + tasks.map((task) => task.id) + ); + + await tryber.tables.WpAppqCampaignTaskGroup.do().insert( + groups.map((group) => ({ + ...group, + task_id: taskMap.get(group.task_id), + })) + ); + } + + private async duplicateMailMerge(campaignId: number) { + if (!this.duplicate.mailMergesFrom) return; + + const mailMerges = await tryber.tables.WpAppqCronJobs.do() + .select( + "display_name", + "email_template_id", + "template_text", + "template_json", + "last_editor_id", + "template_id" + ) + .where("campaign_id", this.duplicate.mailMergesFrom) + .where("email_template_id", ">", 0); + + await tryber.tables.WpAppqCronJobs.do().insert( + mailMerges.map((mailMerge) => ({ + ...mailMerge, + campaign_id: campaignId, + creation_date: tryber.fn.now(), + update_date: tryber.fn.now(), + executed_on: "", + })) + ); + } + + private async duplicatePages(campaignId: number) { + if (!this.duplicate.pagesFrom) return; + + const campaign = await tryber.tables.WpAppqEvdCampaign.do() + .select("page_manual_id", "page_preview_id") + .where("id", this.duplicate.pagesFrom); + + if (!campaign.length) return; + + const { page_manual_id, page_preview_id } = campaign[0]; + + const manualId = await this.duplicatePage({ + pageId: page_manual_id, + campaignId, + }); + + if (manualId) { + await this.copyTranslations({ + pageId: page_manual_id, + campaignId: campaignId, + }); + + await tryber.tables.WpAppqEvdCampaign.do().update({ + page_manual_id: manualId, + }); + } + + const previewId = await this.duplicatePage({ + pageId: page_preview_id, + campaignId, + }); + + if (previewId) { + await this.copyTranslations({ + pageId: page_preview_id, + campaignId: campaignId, + }); + + await tryber.tables.WpAppqEvdCampaign.do().update({ + page_preview_id: previewId, + }); + } + } + + private async duplicatePage({ + pageId, + campaignId, + }: { + pageId: number; + campaignId: number; + }) { + if (!this.duplicate.pagesFrom) return; + + const page = await tryber.tables.WpPosts.do() + .select() + .where("ID", pageId) + .first(); + + if (!page) return null; + + const { ID, ...rest } = page; + const newPage = await tryber.tables.WpPosts.do() + .insert({ + ...rest, + post_title: rest.post_title.replace( + this.duplicate.pagesFrom.toString(), + campaignId.toString() + ), + }) + .returning("ID"); + + const meta = await tryber.tables.WpPostmeta.do() + .select() + .where("post_id", ID); + + await tryber.tables.WpPostmeta.do().insert( + meta.map((metaItem) => { + const { meta_id, ...rest } = metaItem; + return { + ...rest, + post_id: newPage[0].ID ?? newPage[0], + }; + }) + ); + + const newId = newPage[0].ID ?? newPage[0]; + return newId; + } + + private async copyTranslations({ + pageId, + campaignId, + }: { + pageId: number; + campaignId: number; + }) { + if (!this.duplicate.pagesFrom) return; + + const polylangOptions = await tryber.tables.WpOptions.do() + .select("option_value") + .where("option_name", "polylang") + .first(); + + if (!polylangOptions) return; + let parsedOptions: { [key: string]: any } = {}; + try { + parsedOptions = unserialize(polylangOptions.option_value); + } catch (e) { + return; + } + if (!("default_lang" in parsedOptions)) return; + const defaultLanguage = parsedOptions.default_lang; + + const translations = await tryber.tables.WpTermRelationships.do() + .select("object_id", "description") + .join( + "wp_term_taxonomy", + "wp_term_taxonomy.term_taxonomy_id", + "wp_term_relationships.term_taxonomy_id" + ) + .where("object_id", pageId) + .where("taxonomy", "post_translations") + .first(); + + if (!translations) return; + + let parsedTranslations: { [key: string]: any } = {}; + try { + parsedTranslations = unserialize(translations.description); + } catch (e) { + return; + } + for (const t in parsedTranslations) { + if (t === defaultLanguage) continue; + + const transPage = await tryber.tables.WpPosts.do() + .select() + .where("ID", parsedTranslations[t]) + .first(); + + if (!transPage) continue; + const { ID, ...rest } = transPage; + const newTransPage = await tryber.tables.WpPosts.do() + .insert({ + ...rest, + post_title: rest.post_title.replace( + this.duplicate.pagesFrom.toString(), + campaignId.toString() + ), + }) + .returning("ID"); + + const transMeta = await tryber.tables.WpPostmeta.do() + .select() + .where("post_id", ID); + + if (transMeta.length) { + await tryber.tables.WpPostmeta.do().insert( + transMeta.map((metaItem) => { + const { meta_id, ...rest } = metaItem; + return { + ...rest, + post_id: newTransPage[0].ID ?? newTransPage[0], + }; + }) + ); + } + } + } + private async assignOlps(campaignId: number) { const roles = this.getBody().roles; if (!roles) return; diff --git a/src/schema.ts b/src/schema.ts index 307f6beaa..835eb5855 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -886,6 +886,39 @@ export interface components { ProspectStatus: "draft" | "confirmed" | "done"; /** CountryCode */ CountryCode: string; + DossierCreationData: { + project: number; + testType: number; + title: { + customer: string; + tester?: string; + }; + /** Format: date-time */ + startDate: string; + /** Format: date-time */ + endDate?: string; + /** Format: date-time */ + closeDate?: string; + deviceList: number[]; + csm?: number; + roles?: { + role: number; + user: number; + }[]; + description?: string; + productLink?: string; + goal?: string; + outOfScope?: string; + deviceRequirements?: string; + target?: { + notes?: string; + size?: number; + }; + countries?: components["schemas"]["CountryCode"][]; + languages?: number[]; + browsers?: number[]; + productType?: number; + }; }; responses: { /** A user */ @@ -974,45 +1007,7 @@ export interface components { search: string; testerId: string; }; - requestBodies: { - DossierData: { - content: { - "application/json": { - project: number; - testType: number; - title: { - customer: string; - tester?: string; - }; - /** Format: date-time */ - startDate: string; - /** Format: date-time */ - endDate?: string; - /** Format: date-time */ - closeDate?: string; - deviceList: number[]; - csm?: number; - roles?: { - role: number; - user: number; - }[]; - description?: string; - productLink?: string; - goal?: string; - outOfScope?: string; - deviceRequirements?: string; - target?: { - notes?: string; - size?: number; - }; - countries?: components["schemas"]["CountryCode"][]; - languages?: number[]; - browsers?: number[]; - productType?: number; - }; - }; - }; - }; + requestBodies: {}; } export interface operations { @@ -4066,7 +4061,18 @@ export interface operations { }; }; }; - requestBody: components["requestBodies"]["DossierData"]; + requestBody: { + content: { + "application/json": components["schemas"]["DossierCreationData"] & { + duplicate?: { + fields?: number; + useCases?: number; + mailMerges?: number; + pages?: number; + }; + }; + }; + }; }; "get-dossiers-campaign": { parameters: { @@ -4164,7 +4170,11 @@ export interface operations { }; }; }; - requestBody: components["requestBodies"]["DossierData"]; + requestBody: { + content: { + "application/json": components["schemas"]["DossierCreationData"]; + }; + }; }; "get-customers-customer-projects": { parameters: { From e3543a31cbbba9ada7f69a88a56ea7fa209aeb35 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 22 Apr 2024 17:02:46 +0200 Subject: [PATCH 2/7] rework: Refactor polylang handling --- src/routes/dossiers/_post/index.ts | 126 +++++++++++++---------------- 1 file changed, 58 insertions(+), 68 deletions(-) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 560aae2f0..f136fc7dc 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -335,11 +335,6 @@ export default class RouteItem extends AdminRoute<{ }); if (manualId) { - await this.copyTranslations({ - pageId: page_manual_id, - campaignId: campaignId, - }); - await tryber.tables.WpAppqEvdCampaign.do().update({ page_manual_id: manualId, }); @@ -351,15 +346,40 @@ export default class RouteItem extends AdminRoute<{ }); if (previewId) { - await this.copyTranslations({ - pageId: page_preview_id, - campaignId: campaignId, - }); - await tryber.tables.WpAppqEvdCampaign.do().update({ page_preview_id: previewId, }); } + + if (manualId || previewId) { + const defaultLanguage = await this.getDefaultLanguage(); + if (!defaultLanguage) return; + if (manualId) { + const parsedTranslations = await this.getPageTranslationIds({ + pageId: page_manual_id, + defaultLanguage, + }); + for (const t in parsedTranslations) { + await this.duplicatePage({ + pageId: parsedTranslations[t], + campaignId, + }); + } + } + + if (previewId) { + const parsedTranslations = await this.getPageTranslationIds({ + pageId: page_preview_id, + defaultLanguage, + }); + for (const t in parsedTranslations) { + await this.duplicatePage({ + pageId: parsedTranslations[t], + campaignId, + }); + } + } + } } private async duplicatePage({ @@ -393,44 +413,46 @@ export default class RouteItem extends AdminRoute<{ .select() .where("post_id", ID); - await tryber.tables.WpPostmeta.do().insert( - meta.map((metaItem) => { - const { meta_id, ...rest } = metaItem; - return { - ...rest, - post_id: newPage[0].ID ?? newPage[0], - }; - }) - ); + if (meta.length) { + await tryber.tables.WpPostmeta.do().insert( + meta.map((metaItem) => { + const { meta_id, ...rest } = metaItem; + return { + ...rest, + post_id: newPage[0].ID ?? newPage[0], + }; + }) + ); + } const newId = newPage[0].ID ?? newPage[0]; return newId; } - private async copyTranslations({ - pageId, - campaignId, - }: { - pageId: number; - campaignId: number; - }) { - if (!this.duplicate.pagesFrom) return; - + private async getDefaultLanguage() { const polylangOptions = await tryber.tables.WpOptions.do() .select("option_value") .where("option_name", "polylang") .first(); - if (!polylangOptions) return; + if (!polylangOptions) return false; let parsedOptions: { [key: string]: any } = {}; try { parsedOptions = unserialize(polylangOptions.option_value); } catch (e) { - return; + return false; } - if (!("default_lang" in parsedOptions)) return; - const defaultLanguage = parsedOptions.default_lang; + if (!("default_lang" in parsedOptions)) return false; + return parsedOptions.default_lang; + } + private async getPageTranslationIds({ + pageId, + defaultLanguage, + }: { + pageId: number; + defaultLanguage: string; + }) { const translations = await tryber.tables.WpTermRelationships.do() .select("object_id", "description") .join( @@ -442,7 +464,7 @@ export default class RouteItem extends AdminRoute<{ .where("taxonomy", "post_translations") .first(); - if (!translations) return; + if (!translations) return {}; let parsedTranslations: { [key: string]: any } = {}; try { @@ -450,42 +472,10 @@ export default class RouteItem extends AdminRoute<{ } catch (e) { return; } - for (const t in parsedTranslations) { - if (t === defaultLanguage) continue; - - const transPage = await tryber.tables.WpPosts.do() - .select() - .where("ID", parsedTranslations[t]) - .first(); - if (!transPage) continue; - const { ID, ...rest } = transPage; - const newTransPage = await tryber.tables.WpPosts.do() - .insert({ - ...rest, - post_title: rest.post_title.replace( - this.duplicate.pagesFrom.toString(), - campaignId.toString() - ), - }) - .returning("ID"); - - const transMeta = await tryber.tables.WpPostmeta.do() - .select() - .where("post_id", ID); - - if (transMeta.length) { - await tryber.tables.WpPostmeta.do().insert( - transMeta.map((metaItem) => { - const { meta_id, ...rest } = metaItem; - return { - ...rest, - post_id: newTransPage[0].ID ?? newTransPage[0], - }; - }) - ); - } - } + if (defaultLanguage in parsedTranslations) + delete parsedTranslations[defaultLanguage]; + return parsedTranslations; } private async assignOlps(campaignId: number) { From a0a86948bc40c88afc7ccdd9aa7d92c49458a180 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 23 Apr 2024 10:36:33 +0200 Subject: [PATCH 3/7] feat: Add tester duplication --- src/reference/openapi.yml | 4 ++ src/routes/dossiers/_post/duplication.spec.ts | 46 +++++++++++++++++++ src/routes/dossiers/_post/index.ts | 29 ++++++++++++ src/schema.ts | 1 + 4 files changed, 80 insertions(+) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 2b5efa6c0..7bb5f5e84 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -9721,6 +9721,10 @@ paths: type: integer x-stoplight: id: qhyggxgrgajc1 + testers: + type: integer + x-stoplight: + id: lobzb13vjwkln '/dossiers/{campaign}': parameters: - name: campaign diff --git a/src/routes/dossiers/_post/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts index 673066630..feb17710e 100644 --- a/src/routes/dossiers/_post/duplication.spec.ts +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -174,6 +174,16 @@ describe("Route POST /dossiers - duplication", () => { }, { post_id: 2, meta_key: "acf_field", meta_value: "value" }, ]); + await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ + user_id: 1, + campaign_id: 1, + subscription_date: "1970-01-01", + accepted: 1, + devices: "1", + selected_device: 1, + modified: "1970-01-01", + group_id: 1, + }); }); afterAll(async () => { @@ -195,6 +205,7 @@ describe("Route POST /dossiers - duplication", () => { await tryber.tables.WpPostmeta.do().delete(); await tryber.tables.WpTermRelationships.do().delete(); await tryber.tables.WpTermTaxonomy.do().delete(); + await tryber.tables.WpCrowdAppqHasCandidate.do().delete(); }); it("Should return 400 if campaign to duplicate does not exist", async () => { @@ -225,6 +236,13 @@ describe("Route POST /dossiers - duplication", () => { .send({ ...baseRequest, duplicate: { pages: 100 } }); expect(pages.status).toBe(400); + + const testers = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { testers: 100 } }); + + expect(testers.status).toBe(400); }); it("Should duplicate additional fields", async () => { @@ -431,4 +449,32 @@ describe("Route POST /dossiers - duplication", () => { }) ); }); + + it("Should duplicate testers", async () => { + const response = await request(app) + .post("/dossiers") + .set("authorization", "Bearer admin") + .send({ ...baseRequest, duplicate: { testers: 1 } }); + + expect(response.status).toBe(201); + + const id = response.body.id; + + const testers = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select() + .where({ campaign_id: id }); + + expect(testers).toHaveLength(1); + + expect(testers[0]).toMatchObject({ + user_id: 1, + campaign_id: id, + subscription_date: "1970-01-01", + accepted: 1, + devices: "1", + selected_device: 1, + modified: "1970-01-01", + group_id: 1, + }); + }); }); diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index f136fc7dc..1f10f3f16 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -14,6 +14,7 @@ export default class RouteItem extends AdminRoute<{ useCasesFrom?: number; mailMergesFrom?: number; pagesFrom?: number; + testersFrom?: number; } = {}; constructor(config: RouteClassConfiguration) { @@ -27,6 +28,7 @@ export default class RouteItem extends AdminRoute<{ if (duplicate.mailMerges) this.duplicate.mailMergesFrom = duplicate.mailMerges; if (duplicate.pages) this.duplicate.pagesFrom = duplicate.pages; + if (duplicate.testers) this.duplicate.testersFrom = duplicate.testers; } } @@ -65,6 +67,7 @@ export default class RouteItem extends AdminRoute<{ ? [this.duplicate.mailMergesFrom] : []), ...(this.duplicate.pagesFrom ? [this.duplicate.pagesFrom] : []), + ...(this.duplicate.testersFrom ? [this.duplicate.testersFrom] : []), ]), ]; const campaigns = await tryber.tables.WpAppqEvdCampaign.do() @@ -235,6 +238,7 @@ export default class RouteItem extends AdminRoute<{ await this.duplicateUsecases(campaignId); await this.duplicateMailMerge(campaignId); await this.duplicatePages(campaignId); + await this.duplicateTesters(campaignId); } private async duplicateFields(campaignId: number) { @@ -429,6 +433,31 @@ export default class RouteItem extends AdminRoute<{ return newId; } + private async duplicateTesters(campaignId: number) { + if (!this.duplicate.testersFrom) return; + + const testers = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select( + "user_id", + "subscription_date", + "accepted", + "devices", + "selected_device", + "modified", + "group_id" + ) + .where("campaign_id", this.duplicate.testersFrom); + + if (!testers.length) return; + + await tryber.tables.WpCrowdAppqHasCandidate.do().insert( + testers.map((tester) => ({ + ...tester, + campaign_id: campaignId, + })) + ); + } + private async getDefaultLanguage() { const polylangOptions = await tryber.tables.WpOptions.do() .select("option_value") diff --git a/src/schema.ts b/src/schema.ts index 835eb5855..223330016 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -4069,6 +4069,7 @@ export interface operations { useCases?: number; mailMerges?: number; pages?: number; + testers?: number; }; }; }; From 9c2def9e6005dd3fc19aa0859af5dd99f9bc6b74 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 23 Apr 2024 12:39:58 +0200 Subject: [PATCH 4/7] feat: Send new cp to wordpress regeneration api --- deployment/after-install.sh | 1 + .../wp/WordpressJsonApiTrigger/index.spec.ts | 84 +++++++++++++ .../wp/WordpressJsonApiTrigger/index.ts | 36 ++++++ src/routes/dossiers/_post/creation.spec.ts | 1 + src/routes/dossiers/_post/duplication.spec.ts | 65 ++++++++++ src/routes/dossiers/_post/index.spec.ts | 1 + src/routes/dossiers/_post/index.ts | 17 ++- src/routes/dossiers/_post/triggerWp.spec.ts | 116 ++++++++++++++++++ 8 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 src/features/wp/WordpressJsonApiTrigger/index.spec.ts create mode 100644 src/features/wp/WordpressJsonApiTrigger/index.ts create mode 100644 src/routes/dossiers/_post/triggerWp.spec.ts diff --git a/deployment/after-install.sh b/deployment/after-install.sh index 952cbd89e..ce148178a 100644 --- a/deployment/after-install.sh +++ b/deployment/after-install.sh @@ -86,6 +86,7 @@ services: SENTRY_SAMPLE_RATE: ${SENTRY_SAMPLE_RATE:-1} CLOUDFRONT_KEY_ID: ${CLOUDFRONT_KEY_ID} JOTFORM_APIKEY: ${JOTFORM_APIKEY} + WORDPRESS_API_URL: ${WORDPRESS_API_URL} volumes: - /var/docker/keys:/app/keys logging: diff --git a/src/features/wp/WordpressJsonApiTrigger/index.spec.ts b/src/features/wp/WordpressJsonApiTrigger/index.spec.ts new file mode 100644 index 000000000..42d8ac32b --- /dev/null +++ b/src/features/wp/WordpressJsonApiTrigger/index.spec.ts @@ -0,0 +1,84 @@ +import axios from "axios"; +import WordpressJsonApiTrigger from "."; + +// mock axios + +jest.mock("axios"); + +describe("WordpressJsonApiTrigger", () => { + beforeAll(() => { + process.env = Object.assign(process.env, { + WORDPRESS_API_URL: "https://example.com", + }); + }); + + afterAll(() => { + jest.resetAllMocks(); + process.env = Object.assign(process.env, { + WORDPRESS_API_URL: "", + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it("Should create a new instance of WordpressJsonApiTrigger", () => { + const instance = new WordpressJsonApiTrigger(1); + expect(instance).toBeInstanceOf(WordpressJsonApiTrigger); + }); + + it("Should call axios on generateUseCase", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generateUseCase(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { "Content-Type": "application/json" }, + method: "GET", + url: "https://example.com/regenerate-campaign-use-cases/1", + }); + }); + + it("Should call axios on generateMailmerges", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generateMailMerges(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { "Content-Type": "application/json" }, + method: "GET", + url: "https://example.com/regenerate-campaign-crons/1", + }); + }); + it("Should call axios on generatePages", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generatePages(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { "Content-Type": "application/json" }, + method: "GET", + url: "https://example.com/regenerate-campaign-pages/1", + }); + }); + + it("Should call axios on generateTasks", async () => { + const instance = new WordpressJsonApiTrigger(1); + + await instance.generateTasks(); + + expect(axios).toHaveBeenCalledTimes(1); + + expect(axios).toHaveBeenCalledWith({ + headers: { "Content-Type": "application/json" }, + method: "GET", + url: "https://example.com/regenerate-campaign-tasks/1", + }); + }); +}); diff --git a/src/features/wp/WordpressJsonApiTrigger/index.ts b/src/features/wp/WordpressJsonApiTrigger/index.ts new file mode 100644 index 000000000..a5a94e142 --- /dev/null +++ b/src/features/wp/WordpressJsonApiTrigger/index.ts @@ -0,0 +1,36 @@ +import axios from "axios"; + +class WordpressJsonApiTrigger { + constructor(private campaign: number) {} + + public async generateUseCase() { + await this.postToWordpress( + `regenerate-campaign-use-cases/${this.campaign}` + ); + } + + public async generateMailMerges() { + await this.postToWordpress(`regenerate-campaign-crons/${this.campaign}`); + } + + public async generatePages() { + await this.postToWordpress(`regenerate-campaign-pages/${this.campaign}`); + } + + public async generateTasks() { + await this.postToWordpress(`regenerate-campaign-tasks/${this.campaign}`); + } + + private async postToWordpress(url: string) { + await axios({ + method: "GET", + url: `${process.env.WORDPRESS_API_URL}/${url}`, + headers: { + "User-Agent": "Tryber API", + "Content-Type": "application/json", + }, + }); + } +} + +export default WordpressJsonApiTrigger; diff --git a/src/routes/dossiers/_post/creation.spec.ts b/src/routes/dossiers/_post/creation.spec.ts index 402ce6a41..07a42fb2b 100644 --- a/src/routes/dossiers/_post/creation.spec.ts +++ b/src/routes/dossiers/_post/creation.spec.ts @@ -2,6 +2,7 @@ import app from "@src/app"; import { tryber } from "@src/features/database"; import request from "supertest"; +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); const baseRequest = { project: 10, testType: 10, diff --git a/src/routes/dossiers/_post/duplication.spec.ts b/src/routes/dossiers/_post/duplication.spec.ts index feb17710e..794099397 100644 --- a/src/routes/dossiers/_post/duplication.spec.ts +++ b/src/routes/dossiers/_post/duplication.spec.ts @@ -1,7 +1,10 @@ import app from "@src/app"; import { tryber } from "@src/features/database"; +import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; import request from "supertest"; +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); + const baseRequest = { project: 1, testType: 1, @@ -206,6 +209,8 @@ describe("Route POST /dossiers - duplication", () => { await tryber.tables.WpTermRelationships.do().delete(); await tryber.tables.WpTermTaxonomy.do().delete(); await tryber.tables.WpCrowdAppqHasCandidate.do().delete(); + + jest.clearAllMocks(); }); it("Should return 400 if campaign to duplicate does not exist", async () => { @@ -477,4 +482,64 @@ describe("Route POST /dossiers - duplication", () => { group_id: 1, }); }); + + it("Should not post to wordpress if usecase is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { useCases: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generateUseCase + ).not.toHaveBeenCalled(); + }); + + it("Should not post to wordpress if mailmerge is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { mailMerges: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generateMailMerges + ).not.toHaveBeenCalled(); + }); + + it("Should not post to wordpress if mailmerge is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { mailMerges: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generateMailMerges + ).not.toHaveBeenCalled(); + }); + + it("Should not post to wordpress if pages is duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send({ ...baseRequest, duplicate: { pages: 1 } }) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const id = response.body.id; + + expect( + WordpressJsonApiTrigger.prototype.generatePages + ).not.toHaveBeenCalled(); + }); }); diff --git a/src/routes/dossiers/_post/index.spec.ts b/src/routes/dossiers/_post/index.spec.ts index 356694715..a3465aa61 100644 --- a/src/routes/dossiers/_post/index.spec.ts +++ b/src/routes/dossiers/_post/index.spec.ts @@ -2,6 +2,7 @@ import app from "@src/app"; import { tryber } from "@src/features/database"; import request from "supertest"; +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); const baseRequest = { project: 1, testType: 1, diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 1f10f3f16..308d84fb9 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -3,6 +3,7 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; import AdminRoute from "@src/features/routes/AdminRoute"; +import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; import { unserialize } from "php-unserialize"; export default class RouteItem extends AdminRoute<{ @@ -129,7 +130,7 @@ export default class RouteItem extends AdminRoute<{ const campaignId = await this.createCampaign(); await this.linkRolesToCampaign(campaignId); - await this.copyDataFromDuplication(campaignId); + await this.generateLinkedData(campaignId); this.setSuccess(201, { id: campaignId, @@ -233,11 +234,17 @@ export default class RouteItem extends AdminRoute<{ await this.assignOlps(campaignId); } - private async copyDataFromDuplication(campaignId: number) { + private async generateLinkedData(campaignId: number) { + const apiTrigger = new WordpressJsonApiTrigger(campaignId); await this.duplicateFields(campaignId); - await this.duplicateUsecases(campaignId); - await this.duplicateMailMerge(campaignId); - await this.duplicatePages(campaignId); + if (this.duplicate.useCasesFrom) await this.duplicateUsecases(campaignId); + else await apiTrigger.generateUseCase(); + if (this.duplicate.mailMergesFrom) + await this.duplicateMailMerge(campaignId); + else await apiTrigger.generateMailMerges(); + + if (this.duplicate.pagesFrom) await this.duplicatePages(campaignId); + else await apiTrigger.generatePages(); await this.duplicateTesters(campaignId); } diff --git a/src/routes/dossiers/_post/triggerWp.spec.ts b/src/routes/dossiers/_post/triggerWp.spec.ts new file mode 100644 index 000000000..4d89879d2 --- /dev/null +++ b/src/routes/dossiers/_post/triggerWp.spec.ts @@ -0,0 +1,116 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import WordpressJsonApiTrigger from "@src/features/wp/WordpressJsonApiTrigger"; +import request from "supertest"; + +jest.mock("@src/features/wp/WordpressJsonApiTrigger"); + +const baseRequest = { + project: 1, + testType: 1, + title: { + customer: "Campaign Title for Customer", + tester: "Campaign Title for Tester", + }, + startDate: "2019-08-24T14:15:22Z", + deviceList: [1], +}; + +describe("Route POST /dossiers - duplication", () => { + beforeAll(async () => { + await tryber.tables.WpAppqProject.do().insert({ + id: 1, + display_name: "Test Project", + customer_id: 1, + edited_by: 1, + }); + + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Test Type", + description: "Test Description", + category_id: 1, + }); + + await tryber.tables.WpAppqEvdPlatform.do().insert([ + { + id: 1, + name: "Test Type", + form_factor: 0, + architecture: 1, + }, + ]); + }); + + beforeEach(async () => {}); + + afterAll(async () => { + await tryber.tables.WpAppqProject.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpAppqEvdPlatform.do().delete(); + }); + afterEach(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.CampaignDossierData.do().delete(); + await tryber.tables.CampaignDossierDataBrowsers.do().delete(); + await tryber.tables.CampaignDossierDataCountries.do().delete(); + await tryber.tables.CampaignDossierDataLanguages.do().delete(); + await tryber.tables.WpAppqCampaignAdditionalFields.do().delete(); + + jest.clearAllMocks(); + }); + + it("Should post to wordpress if usecase is not duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generateUseCase + ).toHaveBeenCalledTimes(1); + }); + + it("Should post to wordpress if pages is not duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generatePages + ).toHaveBeenCalledTimes(1); + }); + + it("Should post to wordpress if mailmerge is not duplicated", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generateMailMerges + ).toHaveBeenCalledTimes(1); + }); +}); From cf22ed77914a64a5b2981f921b3ef4fb60920ce6 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 23 Apr 2024 12:44:27 +0200 Subject: [PATCH 5/7] fix: Aggiungi header User-Agent alle richieste GET al servizio di rigenerazione di Wordpress --- .../wp/WordpressJsonApiTrigger/index.spec.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/features/wp/WordpressJsonApiTrigger/index.spec.ts b/src/features/wp/WordpressJsonApiTrigger/index.spec.ts index 42d8ac32b..0c9e888d6 100644 --- a/src/features/wp/WordpressJsonApiTrigger/index.spec.ts +++ b/src/features/wp/WordpressJsonApiTrigger/index.spec.ts @@ -35,7 +35,10 @@ describe("WordpressJsonApiTrigger", () => { expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith({ - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, method: "GET", url: "https://example.com/regenerate-campaign-use-cases/1", }); @@ -49,7 +52,10 @@ describe("WordpressJsonApiTrigger", () => { expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith({ - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, method: "GET", url: "https://example.com/regenerate-campaign-crons/1", }); @@ -62,7 +68,10 @@ describe("WordpressJsonApiTrigger", () => { expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith({ - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, method: "GET", url: "https://example.com/regenerate-campaign-pages/1", }); @@ -76,7 +85,10 @@ describe("WordpressJsonApiTrigger", () => { expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith({ - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "User-Agent": "Tryber API", + }, method: "GET", url: "https://example.com/regenerate-campaign-tasks/1", }); From ea4e1594f1ecc658385173c2e898b46137eeeea2 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 23 Apr 2024 13:14:59 +0200 Subject: [PATCH 6/7] feat: Add generate tasks --- src/routes/dossiers/_post/index.ts | 10 ++++++++-- src/routes/dossiers/_post/triggerWp.spec.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 308d84fb9..84f6270fe 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -236,16 +236,22 @@ export default class RouteItem extends AdminRoute<{ private async generateLinkedData(campaignId: number) { const apiTrigger = new WordpressJsonApiTrigger(campaignId); - await this.duplicateFields(campaignId); + + await apiTrigger.generateTasks(); + + if (this.duplicate.fieldsFrom) await this.duplicateFields(campaignId); + if (this.duplicate.useCasesFrom) await this.duplicateUsecases(campaignId); else await apiTrigger.generateUseCase(); + if (this.duplicate.mailMergesFrom) await this.duplicateMailMerge(campaignId); else await apiTrigger.generateMailMerges(); if (this.duplicate.pagesFrom) await this.duplicatePages(campaignId); else await apiTrigger.generatePages(); - await this.duplicateTesters(campaignId); + + if (this.duplicate.testersFrom) await this.duplicateTesters(campaignId); } private async duplicateFields(campaignId: number) { diff --git a/src/routes/dossiers/_post/triggerWp.spec.ts b/src/routes/dossiers/_post/triggerWp.spec.ts index 4d89879d2..53710322c 100644 --- a/src/routes/dossiers/_post/triggerWp.spec.ts +++ b/src/routes/dossiers/_post/triggerWp.spec.ts @@ -113,4 +113,22 @@ describe("Route POST /dossiers - duplication", () => { WordpressJsonApiTrigger.prototype.generateMailMerges ).toHaveBeenCalledTimes(1); }); + + it("Should post to wordpress to generate tasks", async () => { + const response = await request(app) + .post("/dossiers") + .send(baseRequest) + .set("Authorization", "Bearer admin"); + + expect(response.status).toBe(201); + + const { id } = response.body; + + expect(WordpressJsonApiTrigger).toHaveBeenCalledTimes(1); + expect(WordpressJsonApiTrigger).toHaveBeenCalledWith(id); + + expect( + WordpressJsonApiTrigger.prototype.generateTasks + ).toHaveBeenCalledTimes(1); + }); }); From 2517aff1220001a1f8f96e9dcf5a3d4fa8879204 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 23 Apr 2024 15:52:31 +0200 Subject: [PATCH 7/7] fix: Allow posting empty data --- src/routes/dossiers/_post/index.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/routes/dossiers/_post/index.ts b/src/routes/dossiers/_post/index.ts index 84f6270fe..e849bc811 100644 --- a/src/routes/dossiers/_post/index.ts +++ b/src/routes/dossiers/_post/index.ts @@ -187,7 +187,7 @@ export default class RouteItem extends AdminRoute<{ const dossierId = dossier[0].id ?? dossier[0]; const countries = this.getBody().countries; - if (countries) { + if (countries?.length) { await tryber.tables.CampaignDossierDataCountries.do().insert( countries.map((country) => ({ campaign_dossier_data_id: dossierId, @@ -197,7 +197,7 @@ export default class RouteItem extends AdminRoute<{ } const languages = this.getBody().languages; - if (languages) { + if (languages?.length) { await tryber.tables.CampaignDossierDataLanguages.do().insert( languages.map((language) => ({ campaign_dossier_data_id: dossierId, @@ -207,7 +207,7 @@ export default class RouteItem extends AdminRoute<{ } const browsers = this.getBody().browsers; - if (browsers) { + if (browsers?.length) { await tryber.tables.CampaignDossierDataBrowsers.do().insert( browsers.map((browser) => ({ campaign_dossier_data_id: dossierId, @@ -221,7 +221,7 @@ export default class RouteItem extends AdminRoute<{ private async linkRolesToCampaign(campaignId: number) { const roles = this.getBody().roles; - if (!roles) return; + if (!roles?.length) return; await tryber.tables.CampaignCustomRoles.do().insert( roles.map((role) => ({ @@ -263,6 +263,8 @@ export default class RouteItem extends AdminRoute<{ cp_id: this.duplicate.fieldsFrom, }); + if (!fields.length) return; + await tryber.tables.WpAppqCampaignAdditionalFields.do().insert( fields.map((field) => { const { id, ...rest } = field; @@ -301,6 +303,8 @@ export default class RouteItem extends AdminRoute<{ tasks.map((task) => task.id) ); + if (!groups.length) return; + await tryber.tables.WpAppqCampaignTaskGroup.do().insert( groups.map((group) => ({ ...group, @@ -324,6 +328,8 @@ export default class RouteItem extends AdminRoute<{ .where("campaign_id", this.duplicate.mailMergesFrom) .where("email_template_id", ">", 0); + if (!mailMerges.length) return; + await tryber.tables.WpAppqCronJobs.do().insert( mailMerges.map((mailMerge) => ({ ...mailMerge, @@ -522,7 +528,7 @@ export default class RouteItem extends AdminRoute<{ private async assignOlps(campaignId: number) { const roles = this.getBody().roles; - if (!roles) return; + if (!roles?.length) return; const roleOlps = await tryber.tables.CustomRoles.do() .select("id", "olp")