From 3935318bd02045cfc5b258f1a791822436538a07 Mon Sep 17 00:00:00 2001 From: Iacopo Leardini Date: Mon, 15 Jul 2024 12:01:30 +0200 Subject: [PATCH 01/25] fix: add groupBy at validApplications query --- src/routes/users/me/campaigns/_get/cap.spec.ts | 14 ++++++++++---- src/routes/users/me/campaigns/_get/index.ts | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/routes/users/me/campaigns/_get/cap.spec.ts b/src/routes/users/me/campaigns/_get/cap.spec.ts index 47722774b..3b5d19720 100644 --- a/src/routes/users/me/campaigns/_get/cap.spec.ts +++ b/src/routes/users/me/campaigns/_get/cap.spec.ts @@ -24,7 +24,7 @@ describe("GET /users/me/campaigns - cap", () => { category_id: 1, }); await tryber.seeds().campaign_statuses(); - await tryber.tables.WpAppqEvdCampaign.do().insert({ + const campaign = { start_date: new Date().toISOString().split("T")[0], end_date: endDate, close_date: closeDate, @@ -42,7 +42,11 @@ describe("GET /users/me/campaigns - cap", () => { is_public: 4, phase_id: 20, desired_number_of_testers: 10, - }); + }; + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { ...campaign, id: 1 }, + { ...campaign, id: 2 }, + ]); await tryber.tables.CampaignDossierData.do().insert({ id: 1, @@ -54,6 +58,7 @@ describe("GET /users/me/campaigns - cap", () => { await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ { campaign_id: 1, user_id: 10, accepted: -1 }, { campaign_id: 1, user_id: 20, accepted: 0 }, + { campaign_id: 2, user_id: 30, accepted: 0 }, ]); }); @@ -71,8 +76,9 @@ describe("GET /users/me/campaigns - cap", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("results"); expect(Array.isArray(response.body.results)).toBe(true); - expect(response.body.results.length).toBe(1); + expect(response.body.results.length).toBe(2); expect(response.body.results[0]).toHaveProperty("visibility"); + expect(response.body.results[0]).toHaveProperty("id", 1); expect(response.body.results[0].visibility).toHaveProperty("freeSpots", 9); }); @@ -83,7 +89,7 @@ describe("GET /users/me/campaigns - cap", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("results"); expect(Array.isArray(response.body.results)).toBe(true); - expect(response.body.results.length).toBe(1); + expect(response.body.results.length).toBe(2); expect(response.body.results[0]).toHaveProperty("visibility"); expect(response.body.results[0].visibility).toHaveProperty( "totalSpots", diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index e3363f045..264eb9e4e 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -355,6 +355,7 @@ class RouteItem extends UserRoute<{ "campaign_id", campaignsWithTarget.map((c) => c.id) ) + .groupBy("campaign_id") .then((res) => res.map((r) => ({ campaign_id: r.campaign_id, From ee39cdf1f8abcd20f2224bb2c24dd5424beafbf8 Mon Sep 17 00:00:00 2001 From: sinatragianpaolo Date: Mon, 17 Jun 2024 23:44:30 +0200 Subject: [PATCH 02/25] refactor(patch-ux): remove insights from body --- src/reference/openapi.yml | 84 --------------------------------------- src/schema.ts | 16 -------- 2 files changed, 100 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 970965644..8551835d3 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -3290,61 +3290,6 @@ paths: type: string usersNumber: type: integer - insights: - type: array - items: - type: object - properties: - id: - type: integer - title: - type: string - description: - type: string - maxLength: 350 - severityId: - type: integer - order: - type: integer - clusterIds: - oneOf: - - type: array - items: - type: integer - - type: string - enum: - - all - videoParts: - type: array - items: - type: object - properties: - id: - type: integer - start: - type: number - end: - type: number - mediaId: - type: integer - description: - type: string - maxLength: 150 - order: - type: integer - required: - - start - - end - - mediaId - - description - - order - required: - - title - - description - - severityId - - order - - clusterIds - - videoParts sentiments: type: array items: @@ -3391,7 +3336,6 @@ paths: required: - goal - usersNumber - - insights - sentiments - methodology - questions @@ -3411,21 +3355,6 @@ paths: methodology: description: Methodology Description type: quantitative - insights: - - id: 5 - title: string - description: string - severityId: 0 - order: 0 - clusterIds: - - 0 - videoParts: - - id: 7 - start: 0 - end: 0 - mediaId: 0 - description: string - order: 0 sentiments: - clusterId: 0 value: 4 @@ -3451,19 +3380,6 @@ paths: methodology: description: Methodology Description type: qualitative - insights: - - title: string - description: string - severityId: 0 - order: 0 - clusterIds: - - 0 - videoParts: - - start: 0 - end: 0 - mediaId: 0 - description: string - order: 0 sentiments: - clusterId: 0 value: 4 diff --git a/src/schema.ts b/src/schema.ts index bd748dcd9..e65595d22 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2030,22 +2030,6 @@ export interface operations { | { goal: string; usersNumber: number; - insights: { - id?: number; - title: string; - description: string; - severityId: number; - order: number; - clusterIds: number[] | "all"; - videoParts: { - id?: number; - start: number; - end: number; - mediaId: number; - description: string; - order: number; - }[]; - }[]; sentiments: { id?: number; clusterId: number; From 88a1940850ec7f49476c09754d1665383629c1b3 Mon Sep 17 00:00:00 2001 From: sinatragianpaolo Date: Tue, 18 Jun 2024 00:00:26 +0200 Subject: [PATCH 03/25] feat: Remove unused code for insights in PATCH /campaigns/{campaignId}/ux --- .../campaignId/ux/_patch/from-draft.spec.ts | 470 ------------------ .../campaignId/ux/_patch/from-publish.spec.ts | 235 --------- .../campaigns/campaignId/ux/_patch/index.ts | 200 -------- .../campaignId/ux/_patch/no-data.spec.ts | 164 ------ 4 files changed, 1069 deletions(-) diff --git a/src/routes/campaigns/campaignId/ux/_patch/from-draft.spec.ts b/src/routes/campaigns/campaignId/ux/_patch/from-draft.spec.ts index 8c95a020f..ab46108ef 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/from-draft.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/from-draft.spec.ts @@ -68,26 +68,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { methodology_type: methodology.type, }); - await tryber.tables.UxCampaignInsights.do().insert({ - id: 1, - campaign_id: 1, - version: 1, - title: "Draft insight", - description: "Draft description", - severity_id: 1, - cluster_ids: "1", - finding_id: 10, - enabled: 1, - }); - - await tryber.tables.UxCampaignVideoParts.do().insert({ - id: 1, - insight_id: 1, - start: 0, - end: 10, - description: "My video", - media_id: 1, - }); await tryber.tables.UxCampaignQuestions.do().insert([ { id: 1, @@ -137,8 +117,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { }); afterEach(async () => { await tryber.tables.UxCampaignData.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); - await tryber.tables.UxCampaignVideoParts.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); await tryber.tables.UxCampaignSentiments.do().delete(); }); @@ -150,7 +128,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology, @@ -171,163 +148,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { ); }); - it("Should disable the insights as draft if the insights are not sent ", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [], - sentiments: [], - questions: [], - methodology, - }); - - const insights = await tryber.tables.UxCampaignInsights.do() - .select() - .where({ enabled: 1 }); - expect(insights).toHaveLength(0); - }); - - it("Should thrown an error if trying to edit an insight that not exists", async () => { - const response = await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 1000, - title: "Draft invalid insight", - description: "Draft invalid description", - severityId: 2, - clusterIds: "all", - order: 0, - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - expect(response.status).toBe(500); - - const insights = await tryber.tables.UxCampaignInsights.do().select(); - expect(insights).toHaveLength(1); - expect(insights[0]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "1", - description: "Draft description", - id: 1, - order: 0, - severity_id: 1, - title: "Draft insight", - version: 1, - }) - ); - }); - - it("Should insert a insights as draft if an item without id is sent", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 1, - title: "Draft insight", - description: "Draft description", - severityId: 1, - clusterIds: [1], - order: 0, - videoParts: [], - }, - { - title: "New insight", - description: "New description", - severityId: 2, - clusterIds: "all", - order: 1, - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const insights = await tryber.tables.UxCampaignInsights.do().select(); - expect(insights).toHaveLength(2); - expect(insights[0]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "1", - description: "Draft description", - order: 0, - severity_id: 1, - finding_id: 10, - title: "Draft insight", - version: 1, - }) - ); - expect(insights[1]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "0", - description: "New description", - order: 1, - severity_id: 2, - finding_id: 11, - title: "New insight", - version: 1, - }) - ); - }); - - it("Should update a insights as draft if an item with id is sent", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 1, - title: "Updated insight", - description: "Updated description", - severityId: 2, - clusterIds: "all", - order: 1, - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const insights = await tryber.tables.UxCampaignInsights.do().select(); - expect(insights).toHaveLength(1); - expect(insights[0]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "0", - description: "Updated description", - order: 1, - severity_id: 2, - title: "Updated insight", - version: 1, - }) - ); - }); - it("Should insert a question as draft if an item without id is sent", async () => { await request(app) .patch("/campaigns/1/ux") @@ -335,7 +155,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [ { @@ -376,7 +195,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [ { @@ -406,7 +224,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [ { id: 1, @@ -445,7 +262,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology: { ...methodology, type: "quali-quantitative" }, @@ -478,7 +294,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology: { ...methodology, description: "The new description" }, @@ -513,7 +328,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "New Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology, @@ -546,7 +360,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 6, - insights: [], sentiments: [], questions: [], methodology, @@ -603,288 +416,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { ); }); - it("Should create a new version of insights on publish", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - status: "publish", - }); - - const insights = await tryber.tables.UxCampaignInsights.do().select(); - expect(insights).toHaveLength(2); - expect(insights[0]).toEqual( - expect.objectContaining({ - campaign_id: 1, - version: 1, - title: "Draft insight", - description: "Draft description", - severity_id: 1, - cluster_ids: "1", - }) - ); - expect(insights[1]).toEqual( - expect.objectContaining({ - campaign_id: 1, - version: 2, - title: "Draft insight", - description: "Draft description", - severity_id: 1, - cluster_ids: "1", - finding_id: 10, - }) - ); - }); - - it("Should remove insight videopart if empty", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 1, - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const videoParts = await tryber.tables.UxCampaignVideoParts.do().select(); - expect(videoParts).toHaveLength(0); - }); - - it("Should add insight videopart as draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 1, - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [ - { - id: 1, - start: 0, - end: 10, - mediaId: 1, - description: "My video", - order: 0, - }, - { - start: 10, - end: 100, - mediaId: 1, - description: "My second video", - order: 1, - }, - ], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const data = await tryber.tables.UxCampaignInsights.do().select(); - expect(data).toHaveLength(1); - const insightId = data[0].id; - const videoParts = await tryber.tables.UxCampaignVideoParts.do().select(); - expect(videoParts).toHaveLength(2); - expect(videoParts[0]).toEqual( - expect.objectContaining({ - start: 0, - end: 10, - media_id: 1, - description: "My video", - order: 0, - insight_id: insightId, - }) - ); - expect(videoParts[1]).toEqual( - expect.objectContaining({ - start: 10, - end: 100, - media_id: 1, - description: "My second video", - order: 1, - insight_id: insightId, - }) - ); - }); - - it("Should update insight videopart as draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 1, - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [ - { - id: 1, - start: 10, - end: 100, - mediaId: 1, - description: "Updated video", - order: 1, - }, - ], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const data = await tryber.tables.UxCampaignInsights.do().select(); - expect(data).toHaveLength(1); - const insightId = data[0].id; - const videoParts = await tryber.tables.UxCampaignVideoParts.do().select(); - expect(videoParts).toHaveLength(1); - expect(videoParts[0]).toEqual( - expect.objectContaining({ - start: 10, - end: 100, - media_id: 1, - description: "Updated video", - order: 1, - insight_id: insightId, - }) - ); - }); - - it("Should insert new insight with videopart as draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - title: "My new insight", - description: "My new description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [ - { - start: 10, - end: 100, - mediaId: 1, - description: "New video", - order: 0, - }, - ], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const data = await tryber.tables.UxCampaignInsights.do() - .select() - .where({ enabled: 1 }); - expect(data).toHaveLength(1); - expect(data[0]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "0", - description: "My new description", - order: 0, - severity_id: 1, - title: "My new insight", - version: 1, - finding_id: 11, - }) - ); - const insightId = data[0].id; - const videoParts = await tryber.tables.UxCampaignVideoParts.do().select(); - expect(videoParts).toHaveLength(1); - expect(videoParts[0]).toEqual( - expect.objectContaining({ - start: 10, - end: 100, - media_id: 1, - description: "New video", - order: 0, - insight_id: insightId, - }) - ); - }); - - it("Should create a new version of videoparts on publish", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - status: "publish", - }); - - const publishInsight = await tryber.tables.UxCampaignInsights.do() - .select() - .where({ - version: 1, - campaign_id: 1, - }) - .first(); - - const draftInsight = await tryber.tables.UxCampaignInsights.do() - .select() - .where({ - version: 2, - campaign_id: 1, - }) - .first(); - expect(publishInsight).toBeDefined(); - expect(draftInsight).toBeDefined(); - - if (!publishInsight || !draftInsight) throw new Error("Insight not found"); - - const publishVideoParts = await tryber.tables.UxCampaignVideoParts.do() - .select() - .where({ - insight_id: publishInsight.id, - }) - .first(); - - const draftVideoPart = await tryber.tables.UxCampaignVideoParts.do() - .select() - .where({ - insight_id: draftInsight.id, - }) - .first(); - - expect(publishVideoParts).toBeDefined(); - expect(draftVideoPart).toBeDefined(); - }); - it("Should create a new version of questions on publish", async () => { const questionsBeforePatch = await tryber.tables.UxCampaignQuestions.do() .select() @@ -991,7 +522,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [ { id: 1, diff --git a/src/routes/campaigns/campaignId/ux/_patch/from-publish.spec.ts b/src/routes/campaigns/campaignId/ux/_patch/from-publish.spec.ts index 85c59f35b..72fff29a3 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/from-publish.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/from-publish.spec.ts @@ -68,49 +68,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { }, ]); - await tryber.tables.UxCampaignInsights.do().insert([ - { - id: 1, - campaign_id: 1, - version: 1, - title: "Publish insight", - description: "Publish description", - severity_id: 1, - cluster_ids: "1", - finding_id: 10, - enabled: 1, - }, - { - id: 2, - campaign_id: 1, - version: 2, - title: "Draft insight", - description: "Draft description", - severity_id: 1, - cluster_ids: "1", - finding_id: 20, - enabled: 1, - }, - ]); - - await tryber.tables.UxCampaignVideoParts.do().insert([ - { - id: 1, - media_id: 1, - insight_id: 1, - start: 0, - end: 10, - description: "Publish video part", - }, - { - id: 2, - media_id: 1, - insight_id: 1, - start: 0, - end: 10, - description: "Draft video part", - }, - ]); await tryber.tables.UxCampaignQuestions.do().insert([ { id: 1, @@ -135,8 +92,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { afterEach(async () => { await tryber.tables.UxCampaignData.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); - await tryber.tables.UxCampaignVideoParts.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); }); @@ -147,7 +102,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology, @@ -175,52 +129,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { ); }); - it("Should disable insights from the draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [], - sentiments: [], - questions: [], - methodology, - }); - - const data = await tryber.tables.UxCampaignInsights.do().select(); - - expect(data).toHaveLength(2); - expect(data[0]).toEqual( - expect.objectContaining({ - id: 1, - campaign_id: 1, - cluster_ids: "1", - description: "Publish description", - order: 0, - severity_id: 1, - title: "Publish insight", - version: 1, - finding_id: 10, - enabled: 1, - }) - ); - expect(data[1]).toEqual( - expect.objectContaining({ - id: 2, - campaign_id: 1, - cluster_ids: "1", - description: "Draft description", - order: 0, - severity_id: 1, - title: "Draft insight", - version: 2, - finding_id: 20, - enabled: 0, - }) - ); - }); - it("Should update a methodology description in the draft", async () => { const draftBefore = await tryber.tables.UxCampaignData.do() .select("methodology_description") @@ -232,7 +140,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology: { ...methodology, description: "New description" }, @@ -259,7 +166,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology: { ...methodology, type: "quantitative" }, @@ -287,7 +193,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [{ name: "Updated Draft Question", id: 2 }], methodology, @@ -312,7 +217,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { .send({ goal: "New Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology, @@ -337,7 +241,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { .send({ goal: "Test Goal", usersNumber: 6, - insights: [], sentiments: [], questions: [], methodology, @@ -350,142 +253,4 @@ describe("PATCH /campaigns/{campaignId}/ux - from publish", () => { expect(updatedDraft?.users).not.toEqual(draftBefore?.users); expect(updatedDraft?.users).toEqual(6); }); - - it("Should insert a insights in the draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 2, - title: "Draft insight", - description: "Draft description", - severityId: 1, - clusterIds: [1], - order: 0, - videoParts: [], - }, - { - title: "New insight", - description: "New description", - severityId: 2, - clusterIds: "all", - order: 1, - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const insights = await tryber.tables.UxCampaignInsights.do().select(); - expect(insights).toHaveLength(3); - expect(insights[0]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "1", - description: "Publish description", - order: 0, - severity_id: 1, - title: "Publish insight", - version: 1, - }) - ); - - expect(insights[1]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "1", - description: "Draft description", - order: 0, - severity_id: 1, - title: "Draft insight", - version: 2, - }) - ); - expect(insights[2]).toEqual( - expect.objectContaining({ - campaign_id: 1, - cluster_ids: "0", - description: "New description", - order: 1, - severity_id: 2, - title: "New insight", - version: 2, - }) - ); - }); - - it("Should insert a insights video part in the draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - id: 2, - title: "Draft insight", - description: "Draft description", - severityId: 1, - clusterIds: [1], - order: 0, - videoParts: [ - { - id: 2, - start: 0, - end: 10, - mediaId: 1, - description: "Draft video part", - order: 0, - }, - { - start: 10, - end: 100, - mediaId: 1, - description: "New video part", - order: 1, - }, - ], - }, - { - title: "New insight", - description: "New description", - severityId: 2, - clusterIds: "all", - order: 1, - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const videoPart = await tryber.tables.UxCampaignVideoParts.do().select(); - expect(videoPart).toHaveLength(3); - expect(videoPart[0]).toEqual( - expect.objectContaining({ - id: 1, - description: "Publish video part", - }) - ); - expect(videoPart[1]).toEqual( - expect.objectContaining({ - id: 2, - description: "Draft video part", - }) - ); - expect(videoPart[2]).toEqual( - expect.objectContaining({ - id: 3, - description: "New video part", - }) - ); - }); }); diff --git a/src/routes/campaigns/campaignId/ux/_patch/index.ts b/src/routes/campaigns/campaignId/ux/_patch/index.ts index 46666df4f..5c74af590 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/index.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/index.ts @@ -35,57 +35,14 @@ export default class PatchUx extends UserRoute<{ return this.setNoAccessError(); } - if (await this.isThereNonExistingMediaInBody()) { - this.setError(400, new OpenapiError(`Media not found`)); - return false; - } - if (this.invalidSentimentsValues()) { this.setError(500, new OpenapiError(`Sentiment values are invalid`)); throw new OpenapiError(`Sentiment values are invalid`); } - if (this.thereAreInvalidFindingIds()) { - this.setError(500, new OpenapiError(`Insight not found`)); - throw new OpenapiError(`Insights with id not found`); - } - return true; } - private async isThereNonExistingMediaInBody() { - const body = this.getBody(); - if (!("status" in body)) { - const { insights } = body; - if (insights) { - const videoParts = insights.flatMap((i) => i.videoParts || []); - const mediaIds = videoParts.map((v) => v.mediaId); - const media = await tryber.tables.WpAppqUserTaskMedia.do() - .select() - .whereIn("id", mediaIds); - if ([...new Set(media)].length !== [...new Set(mediaIds)].length) { - return true; - } - } - } - return false; - } - - private thereAreInvalidFindingIds() { - const body = this.getBody(); - if ("status" in body) return false; - const { insights } = body; - const toUpdate = insights.filter((i) => i.id); - const currentInsights = this.lastDraft?.findings || []; - const currentInsightIds = currentInsights.map((i) => i.id); - - const notFoundIds = toUpdate - .map((i) => i.id) - .filter((id) => !currentInsightIds.includes(id as number)); - - return notFoundIds.length > 0; - } - private invalidSentimentsValues() { const body = this.getBody(); if ("status" in body) return false; @@ -136,7 +93,6 @@ export default class PatchUx extends UserRoute<{ } await this.updateUxData(); - await this.updateInsights(); await this.updateQuestions(); await this.updateSentiments(); } @@ -172,13 +128,6 @@ export default class PatchUx extends UserRoute<{ .where({ campaign_id: this.campaignId }); } - private async updateInsights() { - await this.removeFindings(); - - await this.insertNewFindings(); - await this.updateExistingFindings(); - } - private async updateQuestions() { await this.removeQuestions(); await this.insertNewQuestions(); @@ -326,155 +275,6 @@ export default class PatchUx extends UserRoute<{ } } - private async removeFindings() { - const body = this.getBody(); - if ("status" in body) return; - const { insights } = body; - - const toUpdate = insights.filter((i) => i.id); - const currentInsights = this.lastDraft?.findings || []; - const currentInsightIds = currentInsights.map((i) => i.id); - - const toRemove = currentInsightIds.filter( - (id) => !toUpdate.map((i) => i.id).includes(id as number) - ); - - if (toRemove.length) { - await tryber.tables.UxCampaignInsights.do() - .update("enabled", 0) - .whereIn( - "id", - currentInsightIds.filter( - (id) => !toUpdate.map((i) => i.id).includes(id as number) - ) - ); - - await tryber.tables.UxCampaignVideoParts.do() - .delete() - .whereIn( - "insight_id", - currentInsightIds.filter( - (id) => !toUpdate.map((i) => i.id).includes(id as number) - ) - ); - } - } - - private async insertNewFindings() { - const body = this.getBody(); - if ("status" in body) return; - const { insights } = body; - - const toInsert = insights.filter((i) => !i.id); - if (toInsert.length) { - for (const item of toInsert) { - const maxFindingId = await tryber.tables.UxCampaignInsights.do() - .max("finding_id", { as: "max" }) - .first(); - - const insight = await tryber.tables.UxCampaignInsights.do() - .insert({ - campaign_id: this.campaignId, - cluster_ids: - item.clusterIds === "all" ? "0" : item.clusterIds.join(","), - description: item.description, - order: item.order, - severity_id: item.severityId, - title: item.title, - version: this.version, - finding_id: maxFindingId?.max ? maxFindingId?.max + 1 : 1, - }) - .returning("id"); - if (item.videoParts && item.videoParts.length) { - const insightId = insight[0].id ?? insight[0]; - await tryber.tables.UxCampaignVideoParts.do().insert( - item.videoParts.map((v) => ({ - start: v.start, - end: v.end, - media_id: v.mediaId, - description: v.description, - order: v.order, - insight_id: insightId, - })) - ); - } - } - } - } - - private async updateExistingFindings() { - const body = this.getBody(); - if ("status" in body) return; - const { insights } = body; - const updatedFindings = insights.filter((i) => i.id); - - if (updatedFindings.length) { - for (const item of updatedFindings) { - await tryber.tables.UxCampaignInsights.do() - .update({ - cluster_ids: - item.clusterIds === "all" ? "0" : item.clusterIds.join(","), - description: item.description, - order: item.order, - severity_id: item.severityId, - title: item.title, - version: this.version, - }) - .where({ - id: item.id, - }); - - const newVideoParts = item.videoParts.filter((i) => !i.id); - - if (newVideoParts.length) { - await tryber.tables.UxCampaignVideoParts.do().insert( - newVideoParts.map((v) => ({ - start: v.start, - end: v.end, - media_id: v.mediaId, - description: v.description, - order: v.order, - insight_id: item.id, - })) - ); - } - const updatedVideoParts = item.videoParts.filter((i) => i.id); - - const currentVideoParts = (this.lastDraft?.findings || []).flatMap( - (f) => (f.id == item.id && f.videoParts ? f.videoParts : []) - ); - - const currentVideoPartIds = currentVideoParts.map((i) => i.id); - const updatedVideoPartsIds = updatedVideoParts.map((i) => i.id); - - const toRemove = currentVideoPartIds.filter( - (id) => !updatedVideoPartsIds.includes(id as number) - ); - - if (toRemove.length) { - await tryber.tables.UxCampaignVideoParts.do() - .delete() - .whereIn("id", toRemove); - } - - for (const videoPart of updatedVideoParts) { - await tryber.tables.UxCampaignVideoParts.do() - .update({ - start: videoPart.start, - end: videoPart.end, - media_id: videoPart.mediaId, - description: videoPart.description, - order: videoPart.order, - insight_id: item.id, - }) - .where({ - id: videoPart.id, - }); - } - } - } - } - private async publish() { const draftData = this.lastDraft?.data; if (!draftData) { diff --git a/src/routes/campaigns/campaignId/ux/_patch/no-data.spec.ts b/src/routes/campaigns/campaignId/ux/_patch/no-data.spec.ts index f3355d15b..ec27d2ada 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/no-data.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/no-data.spec.ts @@ -59,8 +59,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { afterEach(async () => { await tryber.tables.UxCampaignData.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); - await tryber.tables.UxCampaignVideoParts.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); await tryber.tables.UxCampaignSentiments.do().delete(); await tryber.tables.WpAppqUsecaseCluster.do().delete(); @@ -73,16 +71,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [ - { - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [], - }, - ], sentiments: [], questions: [], methodology, @@ -102,71 +90,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { ); }); - it("Should insert insight as draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - const data = await tryber.tables.UxCampaignInsights.do().select(); - expect(data).toHaveLength(1); - expect(data[0]).toEqual( - expect.objectContaining({ - cluster_ids: "0", - description: "My description", - order: 0, - severity_id: 1, - title: "My insight", - }) - ); - }); - - it("Should insert insight as draft with correct finding_id", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const data = await tryber.tables.UxCampaignInsights.do().select(); - expect(data).toHaveLength(1); - expect(data[0]).toEqual( - expect.objectContaining({ - finding_id: 1, - }) - ); - }); - it("Should insert question as draft", async () => { await request(app) .patch("/campaigns/1/ux") @@ -174,7 +97,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [{ name: "Is there life on Mars?" }], methodology, @@ -198,7 +120,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [{ value: 5, clusterId: 1, comment: "My comment" }], questions: [], methodology, @@ -218,53 +139,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { ); }); - it("Should insert insight videopart as draft", async () => { - await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [ - { - start: 0, - end: 10, - mediaId: 1, - description: "My video", - order: 0, - }, - ], - }, - ], - sentiments: [], - questions: [], - methodology, - }); - - const data = await tryber.tables.UxCampaignInsights.do().select(); - expect(data).toHaveLength(1); - const insightId = data[0].id; - const videoParts = await tryber.tables.UxCampaignVideoParts.do().select(); - expect(videoParts).toHaveLength(1); - expect(videoParts[0]).toEqual( - expect.objectContaining({ - start: 0, - end: 10, - media_id: 1, - description: "My video", - order: 0, - insight_id: insightId, - }) - ); - }); - it("Should insert methodology type as draft", async () => { await request(app) .patch("/campaigns/1/ux") @@ -272,7 +146,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology, @@ -294,7 +167,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], @@ -316,7 +188,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], @@ -338,7 +209,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { .send({ goal: "Test Goal", usersNumber: 6, - insights: [], sentiments: [], questions: [], @@ -353,40 +223,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from empty", () => { expect(data?.version).toEqual(1); }); - it("Should return 400 if inserting video part with invalid media id", async () => { - const response = await request(app) - .patch("/campaigns/1/ux") - .set("Authorization", "Bearer admin") - .send({ - goal: "Test Goal", - usersNumber: 5, - insights: [ - { - title: "My insight", - description: "My description", - severityId: 1, - order: 0, - clusterIds: "all", - videoParts: [ - { - start: 0, - end: 10, - mediaId: 99, - description: "My video", - order: 0, - }, - ], - }, - ], - sentiments: [], - questions: [], - - methodology, - }); - - expect(response.status).toBe(400); - }); - it("Should return 400 on publish", async () => { const response = await request(app) .patch("/campaigns/1/ux") From 337d826a078d1f30b6e6d54ddb35537755be8c71 Mon Sep 17 00:00:00 2001 From: sinatragianpaolo Date: Tue, 18 Jun 2024 12:45:24 +0200 Subject: [PATCH 04/25] Remove unused code for insights in PATCH /campaigns/{campaignId}/ux --- .../ux/_patch/delete-sentiments.spec.ts | 1 - .../ux/_patch/draft-modified.spec.ts | 102 ------------------ .../campaignId/ux/_patch/index.spec.ts | 1 - .../campaigns/campaignId/ux/_patch/index.ts | 39 ------- 4 files changed, 143 deletions(-) diff --git a/src/routes/campaigns/campaignId/ux/_patch/delete-sentiments.spec.ts b/src/routes/campaigns/campaignId/ux/_patch/delete-sentiments.spec.ts index 9ae4f1bfc..3bd8d672f 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/delete-sentiments.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/delete-sentiments.spec.ts @@ -34,7 +34,6 @@ const campaign = { const requestBody = { goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology: { diff --git a/src/routes/campaigns/campaignId/ux/_patch/draft-modified.spec.ts b/src/routes/campaigns/campaignId/ux/_patch/draft-modified.spec.ts index 5337c80df..610bd26c6 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/draft-modified.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/draft-modified.spec.ts @@ -59,81 +59,12 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft modified", () => { published: 0, }, ]); - - await tryber.tables.UxCampaignInsights.do().insert([ - { - id: 1, - campaign_id: 123, - version: 1, - title: "Publish insight", - description: "Publish description", - severity_id: 1, - cluster_ids: "1", - finding_id: 10, - enabled: 1, - }, - { - id: 2, - campaign_id: 123, - version: 1, - title: "Publish insight 2", - description: "Publish description 2", - severity_id: 1, - cluster_ids: "1", - finding_id: 20, - enabled: 1, - }, - // Draft modified insights - { - id: 3, - campaign_id: 123, - version: 2, - title: "Publish insight", - description: "Publish description", - severity_id: 1, - cluster_ids: "1", - finding_id: 10, - enabled: 1, - }, - { - id: 4, - campaign_id: 123, - version: 2, - title: "Publish insight 2", - description: "Publish description 2", - severity_id: 1, - cluster_ids: "1", - finding_id: 20, - enabled: 1, - }, - ]); - - await tryber.tables.UxCampaignVideoParts.do().insert([ - { - id: 1, - media_id: 1, - insight_id: 2, - start: 0, - end: 10, - description: "Publish video part", - }, - { - id: 2, - media_id: 1, - insight_id: 4, - start: 0, - end: 10, - description: "Publish video part", - }, - ]); }); afterAll(async () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.UxCampaignData.do().delete(); await tryber.tables.WpAppqUserTaskMedia.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); - await tryber.tables.UxCampaignVideoParts.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); }); @@ -144,35 +75,6 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft modified", () => { .send({ goal: "Test Goal", usersNumber: 5, - insights: [ - { - id: 3, - title: "Publish insight", - description: "Publish description", - order: 0, - severityId: 1, - clusterIds: [1], - videoParts: [], - }, - { - id: 4, - title: "Publish insight 2", - description: "Publish description 2", - order: 0, - severityId: 1, - clusterIds: [1], - videoParts: [ - { - id: 2, - order: 0, - start: 0, - end: 10, - mediaId: 1, - description: "Publish video part", - }, - ], - }, - ], sentiments: [], questions: [], methodology, @@ -198,9 +100,5 @@ describe("PATCH /campaigns/{campaignId}/ux - from draft modified", () => { campaign_id: 123, }) ); - - const videoParts = await tryber.tables.UxCampaignVideoParts.do().select(); - - expect(videoParts.length).toBe(2); }); }); diff --git a/src/routes/campaigns/campaignId/ux/_patch/index.spec.ts b/src/routes/campaigns/campaignId/ux/_patch/index.spec.ts index 87d18745d..7f1206ae8 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/index.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/index.spec.ts @@ -21,7 +21,6 @@ const campaign = { const requestBody = { goal: "Test Goal", usersNumber: 5, - insights: [], sentiments: [], questions: [], methodology: { diff --git a/src/routes/campaigns/campaignId/ux/_patch/index.ts b/src/routes/campaigns/campaignId/ux/_patch/index.ts index 5c74af590..656696436 100644 --- a/src/routes/campaigns/campaignId/ux/_patch/index.ts +++ b/src/routes/campaigns/campaignId/ux/_patch/index.ts @@ -283,7 +283,6 @@ export default class PatchUx extends UserRoute<{ } await this.publishData(); - await this.publishInsight(); await this.publishQuestions(); await this.publishSentiments(); this.version++; @@ -353,42 +352,4 @@ export default class PatchUx extends UserRoute<{ } } } - - private async publishInsight() { - const draftData = this.lastDraft?.data; - if (!draftData) throw new OpenapiError("No draft found"); - - let findingOrder = 0; - for (const insight of draftData.findings) { - const insertedInsight = await tryber.tables.UxCampaignInsights.do() - .insert({ - campaign_id: this.campaignId, - cluster_ids: - insight.clusters === "all" - ? "0" - : insight.clusters.map((c) => c.id).join(","), - description: insight.description, - order: findingOrder++, - severity_id: insight.severity.id, - title: insight.title, - version: this.version + 1, - finding_id: insight.findingId, - }) - .returning("id"); - - const insertedInsightId = insertedInsight[0].id ?? insertedInsight[0]; - - let videoPartOrder = 0; - for (const videoPart of insight.videoParts) { - await tryber.tables.UxCampaignVideoParts.do().insert({ - start: videoPart.start, - end: videoPart.end, - media_id: videoPart.mediaId, - description: videoPart.description, - order: videoPartOrder++, - insight_id: insertedInsightId, - }); - } - } - } } From f7c60d730a30aac3a5dc66b6d5e02e9462e09c96 Mon Sep 17 00:00:00 2001 From: sinatragianpaolo Date: Tue, 18 Jun 2024 15:30:19 +0200 Subject: [PATCH 05/25] refactor(get-campaigns-campaign-ux): remove insights and videopart --- src/reference/openapi.yml | 209 ------------------ src/routes/campaigns/campaignId/ux/UxData.ts | 188 +--------------- .../campaigns/campaignId/ux/_get/data.spec.ts | 163 -------------- .../ux/_get/deleted-clusters.spec.ts | 51 ----- .../campaignId/ux/_get/draft-modified.spec.ts | 112 +--------- .../campaigns/campaignId/ux/_get/index.ts | 1 - src/schema.ts | 25 --- 7 files changed, 9 insertions(+), 740 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 8551835d3..3c78f6e4c 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -2820,79 +2820,6 @@ paths: type: string usersNumber: type: integer - insights: - type: array - items: - type: object - properties: - id: - type: integer - title: - type: string - severity: - type: object - required: - - id - - name - properties: - id: - type: integer - name: - type: string - description: - type: string - clusters: - oneOf: - - type: string - enum: - - all - - type: array - items: - type: object - properties: - id: - type: integer - name: - type: string - required: - - id - - name - videoParts: - type: array - items: - type: object - properties: - id: - type: integer - start: - type: number - end: - type: number - mediaId: - type: integer - url: - type: string - streamUrl: - type: string - description: - type: string - poster: - type: string - required: - - id - - start - - end - - mediaId - - url - - streamUrl - - description - required: - - id - - title - - severity - - description - - clusters - - videoParts sentiments: type: array items: @@ -2970,41 +2897,6 @@ paths: name: Usability Test description: Methodology Description type: qualitative - insights: - - id: 1 - title: My insight - description: This is an insight - severity: - id: 1 - name: Minor - clusters: all - videoParts: - - id: 1 - start: 10 - end: 20 - mediaId: 1 - url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' - streamUrl: 'http://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8' - description: This is a video part - - id: 2 - title: My second insight - description: This is another insight - severity: - id: 2 - name: Positive - clusters: - - id: 1 - name: 'UC1: Cart' - - id: 2 - name: 'UC2: Login' - videoParts: - - id: 2 - start: 15 - end: 25 - mediaId: 2 - url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' - streamUrl: 'http://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8' - description: This is a video part sentiments: - id: 1 value: 5 @@ -3032,41 +2924,6 @@ paths: name: Usability Test description: Methodology Description type: quantitative - insights: - - id: 1 - title: My insight - description: This is an insight - severity: - id: 1 - name: Minor - clusters: all - videoParts: - - id: 1 - start: 10 - end: 20 - mediaId: 1 - url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' - streamUrl: 'http://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8' - description: This is a video part - - id: 2 - title: My second insight - description: This is another insight - severity: - id: 2 - name: Positive - clusters: - - id: 1 - name: 'UC1: Cart' - - id: 2 - name: 'UC2: Login' - videoParts: - - id: 2 - start: 15 - end: 25 - mediaId: 2 - url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' - streamUrl: 'http://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8' - description: This is a video part sentiments: - id: 1 value: 5 @@ -3100,72 +2957,6 @@ paths: name: UX Challenge description: UX Challenge type: qualitative - insights: - - id: 70 - title: Malfunzionamento form - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus lorem, maximus sit amet fermentum ac, venenatis eu sapien. In hac habitasse platea dictumst.' - clusters: - - id: 1 - name: UC 1 - Carrello - severity: - id: 1 - name: Minor - videoParts: - - id: 250 - start: 0 - mediaId: 73579 - end: 97 - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus lorem, maximus sit amet fermentum ac, venenatis eu sapien' - url: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T58918/713c9be296e4fb76d0b074947bbe9a56b2807000_1666111033.mp4' - streamUrl: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T58918/713c9be296e4fb76d0b074947bbe9a56b2807000_1666111033-stream.m3u8' - - id: 251 - start: 22 - mediaId: 73577 - end: 101 - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' - url: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T8208/bfc341707a8a3c247926aee8d4efe14f06f5165d_1666101262.mp4' - streamUrl: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T8208/bfc341707a8a3c247926aee8d4efe14f06f5165d_1666101262-stream.m3u8' - - id: 252 - start: 0 - mediaId: 73578 - end: 152 - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' - url: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T11291/91d3f36562a4d2be809c9b1217f591b317f928b4_1666108920.mp4' - streamUrl: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T11291/91d3f36562a4d2be809c9b1217f591b317f928b4_1666108920-stream.m3u8' - - id: 71 - title: Difficoltà di navigazione - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - clusters: - - id: 2 - name: UC 2 - Profilo - severity: - id: 1 - name: Minor - videoParts: - - id: 253 - start: 0 - mediaId: 73580 - end: 102 - description: Lorem ipsum dolor sit amet - url: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T960/19ebc32b3d71e4a3f2858eea298761d44db5d83b_1666112983.mp4' - streamUrl: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T960/19ebc32b3d71e4a3f2858eea298761d44db5d83b_1666112983-stream.m3u8' - - id: 72 - title: Criticità Pagina Prodotto - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus lorem, maximus sit amet fermentum ac, venenatis eu sapien. In hac habitasse platea dictumst. Etiam sodales nibh turpis, at condimentum arcu vehicula eget. Donec sollicitudin dapibus ' - clusters: - - id: 1 - name: UC 1 - Carrello - severity: - id: 2 - name: Major - videoParts: - - id: 254 - start: 0 - mediaId: 73589 - end: 106 - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas tellus lorem, maximus sit amet fermentum ac, venenatis eu sapien. In hac habitasse platea dictumst. Etiam sodales nibh turpis,' - url: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T17968/0af496e75fb6ec367749d1c34a2f9a0dec0f2b4a_1666121805.mp4' - streamUrl: 'https://s3-eu-west-1.amazonaws.com/appq.use-case-media/CP4845/UC19595/T17968/0af496e75fb6ec367749d1c34a2f9a0dec0f2b4a_1666121805-stream.m3u8' sentiments: [] questions: - id: 1 diff --git a/src/routes/campaigns/campaignId/ux/UxData.ts b/src/routes/campaigns/campaignId/ux/UxData.ts index 689319aa0..609858e74 100644 --- a/src/routes/campaigns/campaignId/ux/UxData.ts +++ b/src/routes/campaigns/campaignId/ux/UxData.ts @@ -23,30 +23,6 @@ export default class UxData { } | undefined; - private _findings: { - id: number; - finding_id: number; - campaign_id: number; - version: number; - title: string; - description: string; - severity_id: number; - cluster_ids: string; - order: number; - }[] = []; - - private _videoParts: { - id: number; - media_id: number; - insight_id: number; - start: number; - end: number; - description: string; - location: string; - streamUrl: string; - poster?: string; - }[] = []; - private _questions: { id: number; campaign_id: number; @@ -71,19 +47,13 @@ export default class UxData { if (!data) return { data: undefined }; - const findings = await this.getFindings({ version: data.version }); const clusters = await this.getClusters(); - const findingsIds = findings.map((f) => f.id); - const videoParts = findingsIds.length - ? await this.getVideoParts(findingsIds) - : []; - const questions = await this.getQuestions({ version: data.version }); const sentiments = await this.getSentiments({ version: data.version }); - return { data, findings, clusters, videoParts, questions, sentiments }; + return { data, clusters, questions, sentiments }; } private async getSentiments({ version }: { version: number }) { @@ -107,27 +77,6 @@ export default class UxData { .orderBy("version", "DESC"); } - private async getVideoParts(findingsIds: number[]) { - return await tryber.tables.UxCampaignVideoParts.do() - .select( - tryber.ref("id").withSchema("ux_campaign_video_parts"), - "media_id", - "start", - "end", - "description", - "location", - "insight_id" - ) - .join( - "wp_appq_user_task_media", - "wp_appq_user_task_media.id", - "ux_campaign_video_parts.media_id" - ) - .whereIn("insight_id", findingsIds) - .where("location", "like", "%.mp4") - .orderBy("order", "asc"); - } - private async getUxData({ published }: { published: number }) { return await tryber.tables.UxCampaignData.do() .select() @@ -152,88 +101,30 @@ export default class UxData { .orderBy("version", "DESC"); } - private async getFindings({ version }: { version: number }) { - const result = await tryber.tables.UxCampaignInsights.do() - .select() - .where({ - campaign_id: this.campaignId, - version, - }) - .where({ enabled: 1 }) - .orderBy("order", "asc"); - return await this.filterDeletedClusters(result); - } - public async lastPublished() { - const { data, findings, clusters, videoParts, questions, sentiments } = - await this.getOne({ - published: 1, - }); + const { data, clusters, questions, sentiments } = await this.getOne({ + published: 1, + }); - if (findings) this._findings = findings; if (clusters) this._clusters = clusters; - if (videoParts) this._videoParts = await this.verifyUrls(videoParts); if (questions) this._questions = questions; if (sentiments) this._sentiments = sentiments; this._data = data; } public async lastDraft() { - const { data, findings, clusters, videoParts, questions, sentiments } = - await this.getOne({ - published: 0, - }); + const { data, clusters, questions, sentiments } = await this.getOne({ + published: 0, + }); if (!data) return; this._data = data; - if (findings) this._findings = findings; if (clusters) this._clusters = clusters; - if (videoParts) this._videoParts = await this.verifyUrls(videoParts); if (questions) this._questions = questions; if (sentiments) this._sentiments = sentiments; } - private async filterDeletedClusters( - findings: { - id: number; - finding_id: number; - campaign_id: number; - version: number; - title: string; - description: string; - severity_id: number; - cluster_ids: string; - order: number; - }[] - ) { - let res = []; - - // Get all cluster ids - const clusterIds = await this.getClusterIds(); - //remove deleted clusters from findings - for (const f of findings) { - const fClusterIds = f.cluster_ids.split(",").map(Number); - const validClusterIds = fClusterIds.filter((c) => clusterIds.includes(c)); - if (f.cluster_ids === "0") res.push(f); - else if (validClusterIds.length) { - res.push({ ...f, cluster_ids: validClusterIds.join(",") }); - } - } - - return res; - } - - private async getClusterIds() { - const results = await tryber.tables.WpAppqUsecaseCluster.do() - .where({ campaign_id: this.campaignId }) - .select("id"); - - if (!results.length) return []; - - return results.map((c) => c.id); - } - get version() { return this._data?.version; } @@ -243,49 +134,11 @@ export default class UxData { const { id: i, version: v, published: p, ...data } = this._data; return { ...data, - - findings: this.findings, questions: this.questions, sentiments: this.sentiments, }; } - get findings() { - return this._findings.map((f) => { - const severityName = - f.severity_id in this.SEVERITIES - ? this.SEVERITIES[f.severity_id as keyof typeof this.SEVERITIES] - : "Unknown"; - - const videoParts = this._videoParts.filter((v) => v.insight_id === f.id); - - return { - id: f.id, - title: f.title, - description: f.description, - clusters: evaluateClusters(this._clusters), - severity: { id: f.severity_id, name: severityName }, - videoParts: videoParts.map((v) => ({ - id: v.id, - start: v.start, - mediaId: v.media_id, - end: v.end, - description: v.description, - url: v.location, - streamUrl: v.streamUrl, - poster: v.poster, - })), - findingId: f.finding_id, - }; - - function evaluateClusters(clusters: { id: number; name: string }[]) { - if (f.cluster_ids === "0") return "all" as const; - const clusterIds = f.cluster_ids.split(",").map(Number); - return clusters.filter((c) => clusterIds.includes(c.id)); - } - }); - } - get questions() { return this._questions.map((q) => ({ id: q.id, @@ -310,33 +163,6 @@ export default class UxData { return true; } - async verifyUrls( - videoParts: { - id: number; - media_id: number; - insight_id: number; - start: number; - end: number; - description: string; - location: string; - }[] - ) { - const video = []; - for (const v of videoParts) { - const stream = v.location.replace(".mp4", "-stream.m3u8"); - const isValidStream = await checkUrl(stream); - const poster = v.location.replace(".mp4", ".0000000.jpg"); - const isValidPoster = await checkUrl(poster); - video.push({ - ...v, - location: this.mapToDistribution(v.location), - streamUrl: isValidStream ? this.mapToDistribution(stream) : "", - poster: isValidPoster ? this.mapToDistribution(poster) : undefined, - }); - } - return video; - } - private mapToDistribution(url: string) { return mapToDistribution(url); } diff --git a/src/routes/campaigns/campaignId/ux/_get/data.spec.ts b/src/routes/campaigns/campaignId/ux/_get/data.spec.ts index 6c7ebb5b9..5928296e5 100644 --- a/src/routes/campaigns/campaignId/ux/_get/data.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_get/data.spec.ts @@ -56,56 +56,6 @@ describe("GET /campaigns/{campaignId}/ux - data", () => { goal: "This is the goal of the reasearch", users: 100, }); - await tryber.tables.UxCampaignInsights.do().insert([ - { - id: 1, - campaign_id: 1, - version: 1, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1,2", - order: 1, - finding_id: 10, - enabled: 1, - }, - { - id: 2, - campaign_id: 1, - version: 1, - title: "Test Insight All Cluster", - description: "Test Description All Cluster", - severity_id: 1, - cluster_ids: "0", - order: 0, - finding_id: 20, - enabled: 1, - }, - { - id: 3, - campaign_id: 1, - version: 1, - title: "Test Insight Disabled", - description: "Test Description Disabled", - severity_id: 1, - cluster_ids: "0", - order: 0, - finding_id: 30, - enabled: 0, - }, - { - id: 4, - campaign_id: 2, - version: 1, - title: "Test Insight Other CP", - description: "Test Description Other CP", - severity_id: 1, - cluster_ids: "0", - order: 0, - finding_id: 40, - enabled: 1, - }, - ]); await tryber.tables.WpAppqUsecaseCluster.do().insert([ { id: 1, @@ -127,15 +77,6 @@ describe("GET /campaigns/{campaignId}/ux - data", () => { }, ]); - await tryber.tables.UxCampaignVideoParts.do().insert({ - id: 1, - insight_id: 1, - start: 0, - end: 10, - order: 0, - media_id: 1, - description: "Test Description", - }); await tryber.tables.WpAppqUserTaskMedia.do().insert({ id: 1, campaign_task_id: 1, @@ -189,116 +130,12 @@ describe("GET /campaigns/{campaignId}/ux - data", () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.UxCampaignData.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); await tryber.tables.WpAppqUsecaseCluster.do().delete(); - await tryber.tables.UxCampaignVideoParts.do().delete(); await tryber.tables.WpAppqUserTaskMedia.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); await tryber.tables.UxCampaignSentiments.do().delete(); }); - it("Should return all the enabled findings", async () => { - const response = await request(app) - .get("/campaigns/1/ux") - .set("Authorization", "Bearer admin"); - expect(response.body).toHaveProperty("insights"); - expect(response.body.insights).toHaveLength(2); - expect(response.body.insights[0]).toEqual( - expect.objectContaining({ - title: "Test Insight All Cluster", - description: "Test Description All Cluster", - severity: expect.objectContaining({ - id: 1, - name: "Minor", - }), - clusters: "all", - videoParts: expect.arrayContaining([]), - }) - ); - expect(response.body.insights[1]).toEqual( - expect.objectContaining({ - title: "Test Insight", - description: "Test Description", - severity: expect.objectContaining({ - id: 1, - name: "Minor", - }), - clusters: [ - expect.objectContaining({ - id: 1, - name: "Test Cluster", - }), - expect.objectContaining({ - id: 2, - name: "Test Cluster 2", - }), - ], - videoParts: expect.arrayContaining([]), - }) - ); - }); - - it("Should return all findings of a specific Campaign", async () => { - const response = await request(app) - .get("/campaigns/1/ux") - .set("Authorization", "Bearer admin"); - expect(response.body).toHaveProperty("insights"); - expect(response.body.insights).toHaveLength(2); - expect(response.body.insights).toEqual( - expect.arrayContaining([ - expect.not.objectContaining({ - id: 4, - }), - ]) - ); - }); - - it("Should return the correct ids for each finding", async () => { - const response = await request(app) - .get("/campaigns/1/ux") - .set("Authorization", "Bearer admin"); - expect(response.body).toHaveProperty("insights"); - expect(response.body.insights).toHaveLength(2); - expect(response.body.insights).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 1, - }), - expect.objectContaining({ - id: 2, - }), - ]) - ); - }); - - it("Should return all the video part in a finding", async () => { - const response = await request(app) - .get("/campaigns/1/ux") - .set("Authorization", "Bearer admin"); - expect(response.body).toHaveProperty("insights"); - expect(response.body.insights).toHaveLength(2); - expect(response.body.insights[1]).toEqual( - expect.objectContaining({ - id: 1, - videoParts: expect.arrayContaining([]), - }) - ); - expect(response.body.insights[1].videoParts).toHaveLength(1); - expect(response.body.insights[1].videoParts[0]).toEqual( - expect.objectContaining({ - start: 0, - end: 10, - description: "Test Description", - mediaId: 1, - url: "https://s3.eu-west-1.amazonaws.com/appq.static/ad4fc347f2579800a1920a8be6e181dda0f4b290_1692791543.mp4", - streamUrl: - "https://s3.eu-west-1.amazonaws.com/appq.static/ad4fc347f2579800a1920a8be6e181dda0f4b290_1692791543-stream.m3u8", - poster: - "https://s3.eu-west-1.amazonaws.com/appq.static/ad4fc347f2579800a1920a8be6e181dda0f4b290_1692791543.0000000.jpg", - }) - ); - }); - it("Should return the questions", async () => { const response = await request(app) .get("/campaigns/1/ux") diff --git a/src/routes/campaigns/campaignId/ux/_get/deleted-clusters.spec.ts b/src/routes/campaigns/campaignId/ux/_get/deleted-clusters.spec.ts index 0e9b8d2ad..ae7afa2b7 100644 --- a/src/routes/campaigns/campaignId/ux/_get/deleted-clusters.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_get/deleted-clusters.spec.ts @@ -186,60 +186,9 @@ describe("With draft only", () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqUsecaseCluster.do().delete(); await tryber.tables.UxCampaignData.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); await tryber.tables.UxCampaignSentiments.do().delete(); }); - it("Should not return findings of a deleted clusters", async () => { - const response = await request(app) - .get(`/campaigns/1/ux`) - .set("Authorization", "Bearer admin"); - - expect(response.body.insights.length).toEqual(4); - - expect(response.body.insights).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 1, - }), - expect.objectContaining({ - id: 2, - }), - expect.objectContaining({ - id: 3, - }), - expect.objectContaining({ - id: 6, - }), - ]) - ); - }); - - it("Should remove deleted cluster on returning findings", async () => { - const response = await request(app) - .get(`/campaigns/1/ux`) - .set("Authorization", "Bearer admin"); - - expect(response.body.insights.length).toEqual(4); - expect(response.body.insights).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 1, - }), - expect.objectContaining({ - id: 2, - }), - expect.objectContaining({ - id: 3, - }), - expect.objectContaining({ - id: 6, - clusters: [{ id: 1, name: "Cluster 1" }], - }), - ]) - ); - }); - it("Should return the sentiments if exist the cluster", async () => { const response = await request(app) .get(`/campaigns/1/ux`) diff --git a/src/routes/campaigns/campaignId/ux/_get/draft-modified.spec.ts b/src/routes/campaigns/campaignId/ux/_get/draft-modified.spec.ts index ce287b4e5..29e393f7a 100644 --- a/src/routes/campaigns/campaignId/ux/_get/draft-modified.spec.ts +++ b/src/routes/campaigns/campaignId/ux/_get/draft-modified.spec.ts @@ -30,7 +30,7 @@ const campaign = { customer_title: "Test Customer", }; -describe("GET /campaigns/{campaignId}/ux - draft modified - insight", () => { +describe("GET /campaigns/{campaignId}/ux - draft modified", () => { beforeAll(async () => { await tryber.tables.WpAppqEvdCampaign.do().insert([ { ...campaign, id: 1, campaign_type_id: 10 }, @@ -68,36 +68,11 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - insight", () => { users: 100, }, ]); - await tryber.tables.UxCampaignInsights.do().insert([ - { - campaign_id: 1, - version: 1, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 10, - enabled: 1, - }, - { - campaign_id: 1, - version: 2, - title: "Test Modified", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 20, - enabled: 1, - }, - ]); }); afterAll(async () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); await tryber.tables.UxCampaignData.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); }); it("Should return status published if there are published campaign data and data are not equal to last draft", async () => { @@ -108,7 +83,7 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - insight", () => { }); }); -describe("GET /campaigns/{campaignId}/ux - draft modified - video part", () => { +describe("GET /campaigns/{campaignId}/ux - draft modified", () => { beforeAll(async () => { await tryber.tables.WpAppqEvdCampaign.do().insert([ { ...campaign, id: 1, campaign_type_id: 10 }, @@ -146,42 +121,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - video part", () => { users: 100, }, ]); - await tryber.tables.UxCampaignInsights.do().insert([ - { - id: 1, - campaign_id: 1, - version: 1, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 10, - enabled: 1, - }, - { - id: 2, - campaign_id: 1, - version: 2, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 20, - enabled: 1, - }, - ]); - - await tryber.tables.UxCampaignVideoParts.do().insert({ - id: 1, - insight_id: 1, - start: 0, - end: 10, - order: 0, - media_id: 1, - description: "Test Description", - }); await tryber.tables.WpAppqUserTaskMedia.do().insert({ id: 1, campaign_task_id: 1, @@ -190,15 +129,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - video part", () => { tester_id: 1, }); - await tryber.tables.UxCampaignVideoParts.do().insert({ - id: 2, - insight_id: 2, - start: 0, - end: 100, - order: 0, - media_id: 1, - description: "Test Description", - }); await tryber.tables.WpAppqUserTaskMedia.do().insert({ id: 2, campaign_task_id: 1, @@ -211,8 +141,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - video part", () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); await tryber.tables.UxCampaignData.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); - await tryber.tables.UxCampaignVideoParts.do().delete(); await tryber.tables.WpAppqUserTaskMedia.do().delete(); }); @@ -261,23 +189,11 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - ux data", () => { users: 100, }, ]); - await tryber.tables.UxCampaignInsights.do().insert({ - campaign_id: 1, - version: 1, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 10, - enabled: 1, - }); }); afterAll(async () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.UxCampaignData.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); }); it("Should return methodology", async () => { @@ -365,17 +281,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - questions", () => { users: 100, }, ]); - await tryber.tables.UxCampaignInsights.do().insert({ - campaign_id: 1, - version: 1, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 10, - enabled: 1, - }); await tryber.tables.UxCampaignQuestions.do().insert([ { id: 1, @@ -413,7 +318,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - questions", () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.UxCampaignData.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); }); it("Should return questions of last draft version", async () => { @@ -470,17 +374,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - sentiments", () => { users: 100, }, ]); - await tryber.tables.UxCampaignInsights.do().insert({ - campaign_id: 1, - version: 1, - title: "Test Insight", - description: "Test Description", - severity_id: 1, - cluster_ids: "1", - order: 0, - finding_id: 10, - enabled: 1, - }); await tryber.tables.WpAppqUsecaseCluster.do().insert([ { id: 1, @@ -548,7 +441,6 @@ describe("GET /campaigns/{campaignId}/ux - draft modified - sentiments", () => { await tryber.tables.WpAppqEvdCampaign.do().delete(); await tryber.tables.UxCampaignData.do().delete(); await tryber.tables.WpAppqCampaignType.do().delete(); - await tryber.tables.UxCampaignInsights.do().delete(); await tryber.tables.UxCampaignQuestions.do().delete(); await tryber.tables.UxCampaignSentiments.do().delete(); await tryber.tables.WpAppqUsecaseCluster.do().delete(); diff --git a/src/routes/campaigns/campaignId/ux/_get/index.ts b/src/routes/campaigns/campaignId/ux/_get/index.ts index 2ecaa13fe..eadc0a6de 100644 --- a/src/routes/campaigns/campaignId/ux/_get/index.ts +++ b/src/routes/campaigns/campaignId/ux/_get/index.ts @@ -87,7 +87,6 @@ export default class Route extends UserRoute<{ | "quantitative" | "quali-quantitative", }, - insights: this.draft.data?.findings || [], sentiments: this.draft.data?.sentiments || [], questions: this.draft.data?.questions || [], }); diff --git a/src/schema.ts b/src/schema.ts index e65595d22..491191bb4 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1956,31 +1956,6 @@ export interface operations { status: "draft" | "published" | "draft-modified"; goal: string; usersNumber: number; - insights?: { - id: number; - title: string; - severity: { - id: number; - name: string; - }; - description: string; - clusters: - | "all" - | { - id: number; - name: string; - }[]; - videoParts: { - id: number; - start: number; - end: number; - mediaId: number; - url: string; - streamUrl: string; - description: string; - poster?: string; - }[]; - }[]; sentiments: { id: number; value: number; From 7ee9581c3bcec5e30c096473c60be9ed939bbaa2 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 17 Jul 2024 16:05:05 +0200 Subject: [PATCH 06/25] feat: Return invalid options --- package.json | 2 +- src/reference/openapi.yml | 32 ++-- .../campaigns/forms/formId/_get/index.ts | 21 ++- .../forms/formId/_get/screenout.spec.ts | 142 ++++++++++++++++++ src/schema.ts | 2 + yarn.lock | 8 +- 6 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 src/routes/campaigns/forms/formId/_get/screenout.spec.ts diff --git a/package.json b/package.json index 8ba2dbfa3..32809d21f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "", "license": "ISC", "dependencies": { - "@appquality/tryber-database": "^0.41.1", + "@appquality/tryber-database": "^0.41.9", "@appquality/wp-auth": "^1.0.7", "@googlemaps/google-maps-services-js": "^3.3.7", "@sendgrid/mail": "^7.6.0", diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 3c78f6e4c..bd460ef73 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -10611,8 +10611,7 @@ components: PreselectionFormQuestion: title: PreselectionFormQuestion allOf: - - type: object - properties: + - properties: question: type: string short_name: @@ -10620,16 +10619,14 @@ components: required: - question - oneOf: - - type: object - properties: + - properties: type: type: string enum: - text required: - type - - type: object - properties: + - properties: type: type: string enum: @@ -10640,11 +10637,18 @@ components: type: array items: type: string + invalidOptions: + type: array + x-stoplight: + id: xgjgbndwz4cd3 + items: + x-stoplight: + id: hdc1yjxxy6con + type: string required: - type - options - - type: object - properties: + - properties: type: type: string pattern: '^cuf_[0-9]*$' @@ -10652,10 +10656,17 @@ components: type: array items: type: integer + invalidOptions: + type: array + x-stoplight: + id: ytwf24veu2syh + items: + x-stoplight: + id: adavsvppix2u2 + type: integer required: - type - - type: object - properties: + - properties: type: type: string enum: @@ -10664,6 +10675,7 @@ components: - address required: - type + type: object Project: title: Project type: object diff --git a/src/routes/campaigns/forms/formId/_get/index.ts b/src/routes/campaigns/forms/formId/_get/index.ts index dfb09c20c..9f0e2f88d 100644 --- a/src/routes/campaigns/forms/formId/_get/index.ts +++ b/src/routes/campaigns/forms/formId/_get/index.ts @@ -1,8 +1,8 @@ /** OPENAPI-CLASS: get-campaigns-forms-formId */ -import UserRoute from "@src/features/routes/UserRoute"; -import * as db from "@src/features/db"; import { tryber } from "@src/features/database"; +import * as db from "@src/features/db"; +import UserRoute from "@src/features/routes/UserRoute"; export default class RouteItem extends UserRoute<{ response: StoplightOperations["get-campaigns-forms-formId"]["responses"]["200"]["content"]["application/json"]; @@ -89,18 +89,31 @@ export default class RouteItem extends UserRoute<{ private async getFormFields() { const results = await tryber.tables.WpAppqCampaignPreselectionFormFields.do() - .select("id", "type", "question", "short_name", "options") + .select( + "id", + "type", + "question", + "short_name", + "options", + "invalid_options" + ) .where("form_id", this.getId()) .orderBy("priority", "asc"); return results.map((item) => { return { - ...item, + id: item.id, + type: item.type, + question: item.question, short_name: item.short_name ? item.short_name : undefined, options: isFieldTypeWithOptions(item.type) && item.options ? parseOptions(item.options) : undefined, + invalidOptions: + isFieldTypeWithOptions(item.type) && item.invalid_options + ? parseOptions(item.invalid_options) + : undefined, }; }); diff --git a/src/routes/campaigns/forms/formId/_get/screenout.spec.ts b/src/routes/campaigns/forms/formId/_get/screenout.spec.ts new file mode 100644 index 000000000..2554d4883 --- /dev/null +++ b/src/routes/campaigns/forms/formId/_get/screenout.spec.ts @@ -0,0 +1,142 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("GET /campaigns/forms/{formId} - screenout data", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + title: "My campaign", + platform_id: 1, + start_date: "2021-01-01", + end_date: "2021-01-01", + page_manual_id: 1, + page_preview_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "My campaign", + }); + await tryber.tables.WpAppqCampaignPreselectionForm.do().insert({ + id: 1, + campaign_id: 1, + name: "The form", + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert([ + { + id: 2, + form_id: 1, + question: "Select question", + type: "select", + options: JSON.stringify(["Option 1", "Option 2"]), + priority: 2, + invalid_options: JSON.stringify(["Option 1"]), + }, + { + id: 3, + form_id: 1, + question: "Multiselect question", + type: "multiselect", + options: JSON.stringify(["Option 3", "Option 4"]), + priority: 3, + invalid_options: JSON.stringify(["Option 3"]), + }, + { + id: 4, + form_id: 1, + question: "Radio question", + type: "radio", + options: JSON.stringify(["Yes", "No"]), + priority: 4, + invalid_options: JSON.stringify(["No"]), + }, + { + id: 8, + form_id: 1, + question: "Cuf question", + type: "cuf_1", + options: JSON.stringify([1, 2, 3]), + priority: 1, + invalid_options: JSON.stringify([1]), + }, + ]); + }); + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + }); + + it("Should return the invalid questions for select", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 2, + invalidOptions: ["Option 1"], + }), + ]) + ); + }); + it("Should return the invalid questions for multiselect", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 3, + invalidOptions: ["Option 3"], + }), + ]) + ); + }); + it("Should return the invalid questions for radio", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 4, + invalidOptions: ["No"], + }), + ]) + ); + }); + it("Should return the invalid questions for cuf", async () => { + const response = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("fields"); + expect(response.body.fields).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 8, + invalidOptions: [1], + }), + ]) + ); + }); +}); diff --git a/src/schema.ts b/src/schema.ts index 491191bb4..b3b879a88 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -806,10 +806,12 @@ export interface components { /** @enum {string} */ type: "multiselect" | "select" | "radio"; options: string[]; + invalidOptions?: string[]; } | { type: string; options?: number[]; + invalidOptions?: number[]; } | { /** @enum {string} */ diff --git a/yarn.lock b/yarn.lock index c646e6a81..4229788f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,10 +28,10 @@ "@babel/parser" "^7.22.5" "@babel/traverse" "^7.22.5" -"@appquality/tryber-database@^0.41.1": - version "0.41.1" - resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.41.1.tgz#525e3e61f5ecbab0d634b97dcb0fed35b97dd4b7" - integrity sha512-KJaPGhJF6UKCnb5jWooZg68cjk9w0w/mFo4VkKz/x0m4CTI3eWEk85Sp7gN7lAh8P15CA46ZepFYfxY8il+eQg== +"@appquality/tryber-database@^0.41.9": + version "0.41.9" + resolved "https://registry.yarnpkg.com/@appquality/tryber-database/-/tryber-database-0.41.9.tgz#ccf906ece07052a62959bf315214324b78ecfd9b" + integrity sha512-LR9rWCATzRJDezIGSmGnGALImaNRvUwcWStQsMMkcQkLl3uxouw0VGZKvRz/tVSBl8Jr8bPGiqpgCGwyckngAA== dependencies: better-sqlite3 "^8.1.0" knex "^2.5.1" From 1c51a7a23d9b374a2ee563a013718ed798c815c2 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 17 Jul 2024 16:32:57 +0200 Subject: [PATCH 07/25] feat: Allow creating form with screenout --- src/routes/campaigns/forms/FieldCreator.ts | 11 +- src/routes/campaigns/forms/_post/index.ts | 14 +- .../campaigns/forms/_post/screenout.spec.ts | 218 ++++++++++++++++++ 3 files changed, 231 insertions(+), 12 deletions(-) create mode 100644 src/routes/campaigns/forms/_post/screenout.spec.ts diff --git a/src/routes/campaigns/forms/FieldCreator.ts b/src/routes/campaigns/forms/FieldCreator.ts index 6307607d8..c0045230c 100644 --- a/src/routes/campaigns/forms/FieldCreator.ts +++ b/src/routes/campaigns/forms/FieldCreator.ts @@ -1,5 +1,5 @@ -import * as db from "@src/features/db"; import { tryber } from "@src/features/database"; +import * as db from "@src/features/db"; export default class FieldCreator { private formId: number; @@ -7,6 +7,7 @@ export default class FieldCreator { private question: string; private short_name: string | undefined; private options: string | undefined; + private invalid_options: string | undefined; private id: number | undefined; private priority: number; @@ -23,6 +24,7 @@ export default class FieldCreator { short_name, type, options, + invalid_options, id, priority, }: { @@ -31,6 +33,7 @@ export default class FieldCreator { short_name?: string; type: string; options?: string[] | number[]; + invalid_options?: string[] | number[]; id?: number; priority: number; }) { @@ -42,6 +45,9 @@ export default class FieldCreator { this.question = question; this.short_name = short_name; this.options = options ? JSON.stringify(options) : undefined; + this.invalid_options = options + ? JSON.stringify(invalid_options) + : undefined; this.id = id; this.priority = priority; } @@ -68,6 +74,9 @@ export default class FieldCreator { form_id: this.formId, priority: this.priority, ...(this.options ? { options: this.options } : {}), + ...(this.invalid_options + ? { invalid_options: this.invalid_options } + : {}), ...(this.short_name ? { short_name: this.short_name } : {}), ...(this.id ? { id: this.id } : {}), }) diff --git a/src/routes/campaigns/forms/_post/index.ts b/src/routes/campaigns/forms/_post/index.ts index 78d48cc99..66fc7815b 100644 --- a/src/routes/campaigns/forms/_post/index.ts +++ b/src/routes/campaigns/forms/_post/index.ts @@ -2,8 +2,6 @@ import OpenapiError from "@src/features/OpenapiError"; import { tryber } from "@src/features/database"; -import Campaigns from "@src/features/db/class/Campaigns"; -import PreselectionForms from "@src/features/db/class/PreselectionForms"; import UserRoute from "@src/features/routes/UserRoute"; import FieldCreator from "../FieldCreator"; @@ -12,17 +10,9 @@ export default class RouteItem extends UserRoute<{ body: StoplightOperations["post-campaigns-forms"]["requestBody"]["content"]["application/json"]; }> { private campaignId: number | undefined; - private db: { - forms: PreselectionForms; - campaigns: Campaigns; - }; constructor(options: RouteItem["configuration"]) { super(options); - this.db = { - forms: new PreselectionForms(), - campaigns: new Campaigns(), - }; const body = this.getBody(); if (body.campaign) { this.campaignId = body.campaign; @@ -143,7 +133,9 @@ export default class RouteItem extends UserRoute<{ question: field.question, short_name: field.short_name, type: field.type, - options: field.hasOwnProperty("options") ? field.options : undefined, + options: "options" in field ? field.options : undefined, + invalid_options: + "invalidOptions" in field ? field.invalidOptions : undefined, priority: i++, }); try { diff --git a/src/routes/campaigns/forms/_post/screenout.spec.ts b/src/routes/campaigns/forms/_post/screenout.spec.ts new file mode 100644 index 000000000..daf2820ed --- /dev/null +++ b/src/routes/campaigns/forms/_post/screenout.spec.ts @@ -0,0 +1,218 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const basicCampaign = { + title: "Test Campaign", + customer_title: "Test Campaign", + min_allowed_media: 1, + campaign_type: 0, + bug_lang: 0, + base_bug_internal_id: "I", + start_date: "2020-01-01", + end_date: "2020-01-01", + close_date: "2020-01-01", + campaign_type_id: 1, + os: "", + pm_id: 1, + is_public: 0, + page_manual_id: 0, + page_preview_id: 0, + status_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, +}; + +const basicUserField = { + type: "select", + name: "A select field", + slug: "a-select-field", + custom_user_field_group_id: 10, + extras: "", + placeholder: "", +}; + +describe("POST /campaigns/forms/ - screenout", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 1, + }); + + await tryber.tables.WpAppqCustomUserFieldGroups.do().insert({ + id: 10, + name: "CUF group", + description: "CUF group description", + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + ...basicUserField, + id: 1, + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + ...basicUserField, + id: 2, + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCustomUserField.do().delete(); + }); + + afterEach(async () => { + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + }); + + it("Should save screenout options for select", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Yes or no", + type: "select", + priority: 1, + options: ["Yes", "No"], + invalidOptions: ["No"], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + invalidOptions: ["No"], + }); + }); + it("Should save screenout options for multiselect", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Select one", + type: "multiselect", + priority: 1, + options: ["Blue", "Red", "Yellow"], + invalidOptions: ["Blue", "Red"], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Select one", + invalidOptions: ["Blue", "Red"], + }); + }); + it("Should save screenout options for radio", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Yes or no", + type: "radio", + priority: 1, + options: ["Yes", "No"], + invalidOptions: ["No"], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + invalidOptions: ["No"], + }); + }); + it("Should save screenout options for cuf", async () => { + const response = await request(app) + .post("/campaigns/forms/") + .send({ + name: "My form", + fields: [ + { + question: "Electricity", + type: "cuf_1", + priority: 1, + options: [1, 2, 3], + invalidOptions: [1, 2], + }, + ], + creationDate: "2024-02-23 00:00:00", + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(201); + const { id } = response.body; + + const get = await request(app) + .get(`/campaigns/forms/${id}`) + .set( + "Authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Electricity", + invalidOptions: [1, 2], + }); + }); +}); From a6885d5d5d16bac4b9d502291b97daf5f7e5a4be Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 17 Jul 2024 16:54:55 +0200 Subject: [PATCH 08/25] feat: Allow editing fields --- .../campaigns/forms/formId/_put/index.ts | 6 +- .../forms/formId/_put/screenout.spec.ts | 289 ++++++++++++++++++ 2 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 src/routes/campaigns/forms/formId/_put/screenout.spec.ts diff --git a/src/routes/campaigns/forms/formId/_put/index.ts b/src/routes/campaigns/forms/formId/_put/index.ts index eb91dbd85..e0559e55b 100644 --- a/src/routes/campaigns/forms/formId/_put/index.ts +++ b/src/routes/campaigns/forms/formId/_put/index.ts @@ -1,8 +1,8 @@ /** OPENAPI-CLASS: put-campaigns-forms-formId */ -import UserRoute from "@src/features/routes/UserRoute"; import OpenapiError from "@src/features/OpenapiError"; -import FieldCreator from "../../FieldCreator"; import { tryber } from "@src/features/database"; +import UserRoute from "@src/features/routes/UserRoute"; +import FieldCreator from "../../FieldCreator"; export default class RouteItem extends UserRoute<{ response: StoplightOperations["put-campaigns-forms-formId"]["responses"]["200"]["content"]["application/json"]; @@ -123,6 +123,8 @@ export default class RouteItem extends UserRoute<{ const fieldCreator = new FieldCreator({ ...field, formId: this.getId(), + invalid_options: + "invalidOptions" in field ? field.invalidOptions : undefined, priority: i++, }); await fieldCreator.create(); diff --git a/src/routes/campaigns/forms/formId/_put/screenout.spec.ts b/src/routes/campaigns/forms/formId/_put/screenout.spec.ts new file mode 100644 index 000000000..5b84d2dc2 --- /dev/null +++ b/src/routes/campaigns/forms/formId/_put/screenout.spec.ts @@ -0,0 +1,289 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +const basicCampaign = { + title: "Test Campaign", + customer_title: "Test Campaign", + min_allowed_media: 1, + campaign_type: 0, + bug_lang: 0, + base_bug_internal_id: "I", + start_date: "2020-01-01", + end_date: "2020-01-01", + close_date: "2020-01-01", + campaign_type_id: 1, + os: "", + pm_id: 1, + is_public: 0, + page_manual_id: 0, + page_preview_id: 0, + status_id: 1, + platform_id: 1, + customer_id: 1, + project_id: 1, +}; + +const sampleBody = { + name: "My form", + fields: [], +}; +describe("PUT /campaigns/forms/ - screenout", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 1, + }); + + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 2, + }); + + await tryber.tables.WpAppqEvdCampaign.do().insert({ + ...basicCampaign, + id: 3, + }); + + await tryber.tables.WpAppqCustomUserFieldGroups.do().insert({ + id: 10, + name: "CUF group", + description: "CUF group description", + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 1, + type: "text", + name: "A text field", + slug: "a-text-field", + custom_user_field_group_id: 10, + placeholder: "write something", + extras: "", + }); + + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 2, + type: "multiselect", + name: "A multiselect field", + slug: "a-multiselect-field", + custom_user_field_group_id: 10, + placeholder: "select something", + extras: "", + }); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCustomUserField.do().delete(); + }); + + beforeEach(async () => { + await tryber.tables.WpAppqCampaignPreselectionForm.do().insert([ + { + id: 1, + name: "My empty form", + creation_date: "2020-01-01 23:59:59", + }, + { + id: 2, + name: "My empty form linked to campaign", + campaign_id: 1, + creation_date: "2020-01-01 23:59:59", + }, + { + id: 3, + name: "My form", + creation_date: "2020-01-01 23:59:59", + }, + { + id: 4, + name: "My empty form linked to campaign with access granted", + campaign_id: 2, + creation_date: "2020-01-01 23:59:59", + }, + ]); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 1, + form_id: 3, + type: "text", + question: "My text question", + short_name: "My short_name question", + priority: 3, + }); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 2, + form_id: 3, + question: "My text cuf question", + short_name: "My short_name cuf question", + type: "cuf_1", + priority: 2, + }); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 3, + form_id: 3, + type: "cuf_2", + question: "My cuf_2 cuf question", + short_name: "My short_name cuf_2 cuf question", + options: JSON.stringify([1, 2]), + priority: 4, + }); + + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 4, + form_id: 3, + type: "select", + question: "My select question", + short_name: "My short_name select question", + options: JSON.stringify(["option1", "option2"]), + priority: 1, + }); + }); + + afterEach(async () => { + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + }); + + it("Should save screenout options for select", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Yes or no", + type: "select", + options: ["Yes", "No"], + invalidOptions: ["No"], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + invalidOptions: ["No"], + }); + }); + + it("Should save screenout options for multiselect", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Select one", + type: "multiselect", + options: ["Red", "Blue", "Yellow"], + invalidOptions: ["Red", "Blue"], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Select one", + invalidOptions: ["Red", "Blue"], + }); + }); + + it("Should save screenout options for radio", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Yes or no", + type: "radio", + options: ["Yes", "No"], + invalidOptions: ["No"], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Yes or no", + invalidOptions: ["No"], + }); + }); + + it("Should save screenout options for cuf", async () => { + const response = await request(app) + .put("/campaigns/forms/1") + .send({ + ...sampleBody, + fields: [ + { + question: "Electricity", + type: "cuf_1", + options: [1, 2, 3], + invalidOptions: [1, 2], + }, + ], + }) + .set( + "authorization", + `Bearer tester capability ["manage_preselection_forms"]` + ); + expect(response.status).toBe(200); + + const get = await request(app) + .get("/campaigns/forms/1") + .set( + "authorization", + 'Bearer tester capability ["manage_preselection_forms"]' + ); + + expect(get.status).toBe(200); + expect(get.body.fields).toHaveLength(1); + expect(get.body.fields[0]).toMatchObject({ + question: "Electricity", + invalidOptions: [1, 2], + }); + }); +}); From 33fc62fc2047707e42712e8416e0362aaf49570a Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 18 Jul 2024 12:00:01 +0200 Subject: [PATCH 09/25] feat: Set candidates as invalid based on preselection from invalid questions --- .../db/class/PreselectionFormFields.ts | 4 + .../Questions/CufMultiSelectQuestion.ts | 13 + .../Questions/CufSelectableQuestion.ts | 7 + .../Questions/SelectableQuestion.ts | 26 ++ .../forms/QuestionFactory/Questions/index.ts | 4 + .../campaignId/forms/QuestionFactory/index.ts | 14 +- .../campaigns/campaignId/forms/_post/index.ts | 18 +- .../campaignId/forms/_post/screenout.spec.ts | 389 ++++++++++++++++++ 8 files changed, 463 insertions(+), 12 deletions(-) create mode 100644 src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts diff --git a/src/features/db/class/PreselectionFormFields.ts b/src/features/db/class/PreselectionFormFields.ts index c075ac168..82dcf8c69 100644 --- a/src/features/db/class/PreselectionFormFields.ts +++ b/src/features/db/class/PreselectionFormFields.ts @@ -14,6 +14,7 @@ type PreselectionFormFieldsType = { | "address" | `cuf_${number}`; options: string; + invalid_options: string; question: string; priority: number; }; @@ -26,6 +27,7 @@ class PreselectionFormFieldsObject { options: PreselectionFormFieldsType["options"]; question: PreselectionFormFieldsType["question"]; priority: PreselectionFormFieldsType["priority"]; + invalid_options: PreselectionFormFieldsType["invalid_options"]; constructor(item: PreselectionFormFieldsType) { this.id = item.id; @@ -35,6 +37,7 @@ class PreselectionFormFieldsObject { this.options = item.options; this.question = item.question; this.priority = item.priority; + this.invalid_options = item.invalid_options; } public getOptions(): string[] | number[] { @@ -63,6 +66,7 @@ class Table extends Database<{ "options", "question", "priority", + "invalid_options", ], }); } diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts index ded74e2cc..fc11ca8fb 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufMultiSelectQuestion.ts @@ -152,6 +152,19 @@ class CufMultiselectQuestion extends Question<{ ) ); } + + async isScreenedOut(item: { data: any }) { + if (!this.question.invalid_options) return false; + const values = item.data.value.id; + if (this.options.length === 0 || this.isNoneOfTheAbove(values)) + return false; + for (const v of values) { + if (this.question.invalid_options.includes(v)) { + return true; + } + } + return false; + } } export default CufMultiselectQuestion; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts index e1dc0d2bc..a16f82d93 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/CufSelectableQuestion.ts @@ -125,6 +125,13 @@ class CufSelectQuestion extends Question<{ }); } } + + async isScreenedOut(item: { data: any }) { + if (!this.question.invalid_options) return false; + const value = item.data.value.id; + if (this.options.length === 0 || this.isNoneOfTheAbove(value)) return false; + return this.question.invalid_options.includes(value); + } } export default CufSelectQuestion; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts index 487059640..5c0ca2d67 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/SelectableQuestion.ts @@ -96,6 +96,32 @@ class SelectableQuestion extends Question<{ private isMultiple() { return this.question.type === "multiselect"; } + + async isScreenedOut(item: { data: any }) { + if (!this.question.invalid_options) return false; + if (this.isSingle()) { + return this.isSingleScreenedOut(item.data); + } else if (this.isMultiple()) { + return this.isMultipleScreenedOut(item.data); + } + return false; + } + + private isSingleScreenedOut(data: any) { + const value = data.value.serialized; + return this.question.invalid_options.includes(value); + } + + private isMultipleScreenedOut(data: any) { + const value = data.value.serialized; + for (const v of value) { + if (this.question.invalid_options.includes(v)) { + return true; + } + } + + return false; + } } export default SelectableQuestion; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts index 2dbd62d1f..e0ec13d50 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/Questions/index.ts @@ -28,6 +28,10 @@ class Question { async insertData(item: { campaignId: number; data: any }): Promise { return; } + + async isScreenedOut(item: { data: any }) { + return false; + } } export default Question; diff --git a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts index 81467e18d..46bafbad1 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/QuestionFactory/index.ts @@ -1,14 +1,14 @@ -import PreselectionFormFields from "@src/features/db/class/PreselectionFormFields"; -import CustomUserFields from "@src/features/db/class/CustomUserFields"; import CustomUserFieldExtras from "@src/features/db/class/CustomUserFieldExtras"; +import CustomUserFields from "@src/features/db/class/CustomUserFields"; +import PreselectionFormFields from "@src/features/db/class/PreselectionFormFields"; import Question from "./Questions"; -import SelectableQuestion from "./Questions/SelectableQuestion"; -import CufTextQuestion from "./Questions/CufTextQuestion"; -import CufSelectQuestion from "./Questions/CufSelectableQuestion"; +import AddressQuestion from "./Questions/AddressQuestion"; import CufMultiSelectQuestion from "./Questions/CufMultiSelectQuestion"; -import PhoneQuestion from "./Questions/PhoneQuestion"; +import CufSelectQuestion from "./Questions/CufSelectableQuestion"; +import CufTextQuestion from "./Questions/CufTextQuestion"; import GenderQuestion from "./Questions/GenderQuestion"; -import AddressQuestion from "./Questions/AddressQuestion"; +import PhoneQuestion from "./Questions/PhoneQuestion"; +import SelectableQuestion from "./Questions/SelectableQuestion"; import SimpleTextQuestion from "./Questions/SimpleTextQuestion"; type QuestionType = Awaited< diff --git a/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts b/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts index 6d5bdf502..9509615eb 100644 --- a/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts +++ b/src/routes/users/me/campaigns/campaignId/forms/_post/index.ts @@ -1,5 +1,6 @@ /** OPENAPI-CLASS: post-users-me-campaigns-campaignId-forms */ +import { tryber } from "@src/features/database"; import CampaignApplications from "@src/features/db/class/CampaignApplications"; import Campaigns, { CampaignObject } from "@src/features/db/class/Campaigns"; import Experience from "@src/features/db/class/Experience"; @@ -140,8 +141,8 @@ class RouteItem extends UserRoute<{ protected async prepare(): Promise { try { - await this.handleForm(); - await this.applyToCampaign(); + const isScreenedOut = await this.handleForm(); + await this.applyToCampaign({ isScreenedOut }); await this.addExperiencePoints(); } catch (e) { this.setError(403, e as OpenapiError); @@ -150,12 +151,12 @@ class RouteItem extends UserRoute<{ this.setSuccess(200, {}); } - private async applyToCampaign() { - await this.db.applications.insert({ + private async applyToCampaign({ isScreenedOut }: { isScreenedOut: boolean }) { + await tryber.tables.WpCrowdAppqHasCandidate.do().insert({ campaign_id: this.campaignId, user_id: this.getWordpressId(), devices: this.deviceId.toString(), - accepted: 0, + accepted: isScreenedOut ? -1 : 0, }); } @@ -174,6 +175,7 @@ class RouteItem extends UserRoute<{ private async handleForm() { const questionItems = await this.getQuestionItems(); await this.checkQuestionValidity(questionItems); + let isScreenedOut = false; for (const field of this.form) { if (questionItems.hasOwnProperty(field.question)) { const question = questionItems[field.question]; @@ -181,8 +183,14 @@ class RouteItem extends UserRoute<{ campaignId: this.campaignId, data: field, }); + if (!isScreenedOut) { + isScreenedOut = await question.isScreenedOut({ + data: field, + }); + } } } + return isScreenedOut; } private async checkQuestionValidity(questionItems: { diff --git a/src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts b/src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts new file mode 100644 index 000000000..233d34866 --- /dev/null +++ b/src/routes/users/me/campaigns/campaignId/forms/_post/screenout.spec.ts @@ -0,0 +1,389 @@ +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; + +describe("POST users/me/campaigns/:campaignId/forms - screenout", () => { + beforeEach(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert({ + id: 1, + wp_user_id: 1, + email: "", + employment_id: 1, + education_id: 1, + }); + await tryber.tables.WpAppqEvdCampaign.do().insert({ + id: 1, + start_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + end_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + title: "Cp", + customer_title: "Cp", + is_public: 1, + os: "1,2", + platform_id: 1, + page_preview_id: 1, + page_manual_id: 1, + pm_id: 1, + project_id: 1, + customer_id: 1, + }); + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 1, + slug: "select-field", + name: "Select field", + type: "select", + placeholder: "", + extras: "", + custom_user_field_group_id: 0, + }); + await tryber.tables.WpAppqCustomUserField.do().insert({ + id: 2, + slug: "multiselect-field", + name: "Multiselect field", + type: "multiselect", + placeholder: "", + extras: "", + custom_user_field_group_id: 0, + }); + await tryber.tables.WpAppqCustomUserFieldExtras.do().insert([ + { + id: 1, + custom_user_field_id: 1, + name: "Yes", + }, + { + id: 2, + custom_user_field_id: 2, + name: "Red", + }, + { + id: 11, + custom_user_field_id: 1, + name: "No", + }, + { + id: 22, + custom_user_field_id: 2, + name: "Blue", + }, + { + id: 222, + custom_user_field_id: 2, + name: "Yellow", + }, + ]); + await tryber.tables.WpAppqCampaignPreselectionForm.do().insert({ + id: 1, + campaign_id: 1, + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 1, + form_id: 1, + type: "select", + options: JSON.stringify(["Yes", "No"]), + invalid_options: JSON.stringify(["No"]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 2, + form_id: 1, + type: "multiselect", + options: JSON.stringify(["Blue", "Red", "Yellow"]), + invalid_options: JSON.stringify(["Blue", "Red"]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 3, + form_id: 1, + type: "radio", + options: JSON.stringify(["Yes", "No"]), + invalid_options: JSON.stringify(["No"]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 4, + form_id: 1, + type: "cuf_1", + options: JSON.stringify([1, 11]), + invalid_options: JSON.stringify([1]), + }); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().insert({ + id: 5, + form_id: 1, + type: "cuf_2", + options: JSON.stringify([2, 22, 222]), + invalid_options: JSON.stringify([2, 22]), + }); + await tryber.tables.WpCrowdAppqDevice.do().insert({ + id: 1, + id_profile: 1, + enabled: 1, + platform_id: 1, + }); + }); + + afterEach(async () => { + await tryber.tables.WpAppqEvdProfile.do().delete(); + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpCrowdAppqDevice.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionForm.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormFields.do().delete(); + await tryber.tables.WpAppqCampaignPreselectionFormData.do().delete(); + await tryber.tables.WpAppqExpPoints.do().delete(); + await tryber.tables.WpCrowdAppqHasCandidate.do().delete(); + await tryber.tables.WpAppqCustomUserField.do().delete(); + await tryber.tables.WpAppqCustomUserFieldExtras.do().delete(); + }); + it("Should set candidate with applied -1 if invalid answer is selected for select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 1, + value: { serialized: "No" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if valid answer is selected for select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 1, + value: { serialized: "Yes" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if invalid answer is selected for radio", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 3, + value: { serialized: "No" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if valid answer is selected for radio", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 3, + value: { serialized: "Yes" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if all invalid answers are selected for multiselect", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 2, + value: { serialized: ["Blue", "Red"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied -1 if some invalid answers are selected for multiselect", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 2, + value: { serialized: ["Blue", "Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if all valid answers are selected for multiselect", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 2, + value: { serialized: ["Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if invalid answer is selected for cuf select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 4, + value: { id: 1, serialized: "1" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if valid answer is selected for cuf select", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 4, + value: { id: 11, serialized: "11" }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); + it("Should set candidate with applied -1 if all invalid answers are selected for multiselect cuf", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 5, + value: { id: [2, 22], serialized: ["Red", "Blue"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied -1 if some invalid answers are selected for multiselect cuf", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 5, + value: { id: [2, 222], serialized: ["Red", "Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: -1 }); + }); + it("Should set candidate with applied 0 if all valid answers are selected for multiselect cuf", async () => { + const response = await request(app) + .post("/users/me/campaigns/1/forms") + .send({ + device: [1], + form: [ + { + question: 5, + value: { id: [222], serialized: ["Yellow"] }, + }, + ], + }) + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const candidate = await tryber.tables.WpCrowdAppqHasCandidate.do() + .select("accepted") + .where("campaign_id", 1) + .where("user_id", 1) + .first(); + expect(candidate).toEqual({ accepted: 0 }); + }); +}); From 593d868004a538d5cbc023ecb54fce6c1994d969 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 18 Jul 2024 14:56:48 +0200 Subject: [PATCH 10/25] feat: Allow filtering by candidates and excluded --- src/reference/openapi.yml | 1 + .../candidates/_get/CandidateDevices.ts | 8 +++- .../campaignId/candidates/_get/Candidates.ts | 12 ++++- .../campaignId/candidates/_get/show.spec.ts | 46 ++++++++++++++++--- src/schema.ts | 6 ++- 5 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index bd460ef73..5eed8b228 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -1702,6 +1702,7 @@ paths: - onlyAccepted - onlyCandidates - all + - candidatesAndExcluded in: query name: show description: Show accepted/candidates or both diff --git a/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts b/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts index 7bd45652c..3f9ec3324 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/CandidateDevices.ts @@ -5,7 +5,11 @@ class CandidateDevices implements CandidateData { private campaignId: number; private candidateIds: number[]; private filters?: { os?: string[] }; - private show: "onlyAccepted" | "onlyCandidates" | "all" = "all"; + private show: + | "onlyAccepted" + | "onlyCandidates" + | "all" + | "candidatesAndExcluded" = "all"; private _devices: | { @@ -36,7 +40,7 @@ class CandidateDevices implements CandidateData { campaignId: number; candidateIds: number[]; filters?: { os?: string[] }; - show: "onlyAccepted" | "onlyCandidates" | "all"; + show: "onlyAccepted" | "onlyCandidates" | "all" | "candidatesAndExcluded"; }) { this.candidateIds = candidateIds; this.filters = filters; diff --git a/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts b/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts index 0757f9760..460cccad8 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts @@ -2,13 +2,17 @@ import { tryber } from "@src/features/database"; class Candidates { private campaign_id: number; - private show: "onlyAccepted" | "onlyCandidates" | "all" = "all"; + private show: + | "onlyAccepted" + | "onlyCandidates" + | "all" + | "candidatesAndExcluded" = "all"; constructor({ campaign_id, show, }: { campaign_id: number; - show: "onlyAccepted" | "onlyCandidates" | "all"; + show: "onlyAccepted" | "onlyCandidates" | "all" | "candidatesAndExcluded"; }) { this.campaign_id = campaign_id; this.show = show; @@ -40,8 +44,12 @@ class Candidates { query.where("accepted", 1); } else if (this.show === "onlyCandidates") { query.where("accepted", 0); + } else if (this.show === "candidatesAndExcluded") { + query.whereIn("accepted", [0, -1]); } + console.log(query.toString()); + return await query; } } diff --git a/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts b/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts index c76b425e7..6d17327b1 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts @@ -25,23 +25,25 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { surname: "pluto", education_id: 1, employment_id: 1, + birth_date: "1999-01-01", + total_exp_pts: 90, + sex: 0, }; await tryber.tables.WpAppqEvdProfile.do().insert([ { ...profile, id: 1, wp_user_id: 1, - birth_date: "2000-01-01", - total_exp_pts: 100, - sex: 1, }, { ...profile, id: 2, wp_user_id: 2, - birth_date: "1999-01-01", - total_exp_pts: 90, - sex: 0, + }, + { + ...profile, + id: 3, + wp_user_id: 3, }, ]); @@ -55,6 +57,11 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { user_id: 1, accepted: 0, }, + { + ...candidate, + user_id: 3, + accepted: -1, + }, ]); await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ { @@ -90,6 +97,11 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { id: 3, id_profile: 2, }, + { + ...device, + id: 4, + id_profile: 3, + }, ]); await tryber.tables.WpAppqEvdPlatform.do().insert({ @@ -145,7 +157,7 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { .get("/campaigns/1/candidates?show=all") .set("authorization", `Bearer tester olp {"appq_tester_selection":true}`); expect(response.body).toHaveProperty("results"); - expect(response.body.results).toHaveLength(2); + expect(response.body.results).toHaveLength(3); expect(response.body.results).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -154,6 +166,9 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { expect.objectContaining({ id: 2, }), + expect.objectContaining({ + id: 3, + }), ]) ); }); @@ -168,4 +183,21 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { expect(response.body.results[0].devices).toHaveLength(1); expect(response.body.results[0].devices[0]).toHaveProperty("id", 3); }); + it("Should show candidates and excluded if candidatesAndExcluded", async () => { + const response = await request(app) + .get("/campaigns/1/candidates?show=candidatesAndExcluded") + .set("authorization", `Bearer tester olp {"appq_tester_selection":true}`); + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(2); + expect(response.body.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + }), + expect.objectContaining({ + id: 3, + }), + ]) + ); + }); }); diff --git a/src/schema.ts b/src/schema.ts index b3b879a88..9f75d2c5a 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1427,7 +1427,11 @@ export interface operations { /** Array with min and max */ filterByAge?: unknown; /** Show accepted/candidates or both */ - show?: "onlyAccepted" | "onlyCandidates" | "all"; + show?: + | "onlyAccepted" + | "onlyCandidates" + | "all" + | "candidatesAndExcluded"; }; }; responses: { From b29bfed93ed56090da96337c8c7a737be98f50c7 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 18 Jul 2024 15:03:45 +0200 Subject: [PATCH 11/25] feat: Return candidate status --- src/reference/openapi.yml | 8 +++++++ .../campaignId/candidates/_get/Candidates.ts | 15 ++++++++---- .../campaignId/candidates/_get/index.ts | 1 + .../campaignId/candidates/_get/show.spec.ts | 24 +++++++++++++++++++ src/schema.ts | 2 ++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 5eed8b228..2641dcc9f 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -1248,6 +1248,14 @@ paths: type: string value: type: string + status: + type: string + x-stoplight: + id: 3f1wy22lgngzn + enum: + - candidate + - excluded + - selected required: - id - name diff --git a/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts b/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts index 460cccad8..df3afaafe 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/Candidates.ts @@ -33,7 +33,8 @@ class Candidates { .select( tryber.ref("id").withSchema("wp_appq_evd_profile"), "name", - "surname" + "surname", + "accepted" ) .where("campaign_id", this.campaign_id) .where("name", "<>", "Deleted User") @@ -48,9 +49,15 @@ class Candidates { query.whereIn("accepted", [0, -1]); } - console.log(query.toString()); - - return await query; + return (await query).map((candidate) => ({ + ...candidate, + status: + candidate.accepted === 1 + ? ("selected" as const) + : candidate.accepted === 0 + ? ("candidate" as const) + : ("excluded" as const), + })); } } diff --git a/src/routes/campaigns/campaignId/candidates/_get/index.ts b/src/routes/campaigns/campaignId/candidates/_get/index.ts index a4d3a1c53..6394d764e 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/index.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/index.ts @@ -286,6 +286,7 @@ export default class RouteItem extends UserRoute<{ age: candidate.age, questions: candidate.questions, levels: candidate.levels, + status: candidate.status, }; }), size: candidates.length, diff --git a/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts b/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts index 6d17327b1..87b9f4b37 100644 --- a/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts +++ b/src/routes/campaigns/campaignId/candidates/_get/show.spec.ts @@ -173,6 +173,30 @@ describe("GET /campaigns/:campaignId/candidates - show ", () => { ); }); + it("Should return acceptance status", async () => { + const response = await request(app) + .get("/campaigns/1/candidates?show=all") + .set("authorization", `Bearer tester olp {"appq_tester_selection":true}`); + expect(response.body).toHaveProperty("results"); + expect(response.body.results).toHaveLength(3); + expect(response.body.results).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 1, + status: "candidate", + }), + expect.objectContaining({ + id: 2, + status: "selected", + }), + expect.objectContaining({ + id: 3, + status: "excluded", + }), + ]) + ); + }); + it("Should show selected device if onlyAccepted", async () => { const response = await request(app) .get("/campaigns/1/candidates?show=onlyAccepted") diff --git a/src/schema.ts b/src/schema.ts index 9f75d2c5a..69191d782 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1464,6 +1464,8 @@ export interface operations { title?: string; value?: string; }[]; + /** @enum {string} */ + status?: "candidate" | "excluded" | "selected"; }[]; } & components["schemas"]["PaginationData"]; }; From 2dbfdabe92f565a4e1ea264ed42e0aaae1a10e5d Mon Sep 17 00:00:00 2001 From: "Davide Bizzi (aider)" Date: Thu, 1 Aug 2024 14:04:12 +0200 Subject: [PATCH 12/25] feat: add visibility logic to campaigns based on user application status and availability --- src/routes/users/me/campaigns/_get/index.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index 264eb9e4e..2d590be5f 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -33,6 +33,17 @@ class RouteItem extends UserRoute<{ this.filterBy = query.filterBy || {}; } + private getVisibility(campaign: { + applied: boolean; + start_date: string; + freeSpots?: number; + }): "candidate" | "unavailable" | "available" { + if (campaign.applied) return "candidate"; + if (new Date(campaign.start_date) > new Date() || campaign.freeSpots === 0) + return "unavailable"; + return "available"; + } + private setOrderBy() { const { orderBy } = this.getQuery(); if (!orderBy) return; @@ -113,7 +124,8 @@ class RouteItem extends UserRoute<{ manual_link: cp.manual_link, preview_link: cp.preview_link, applied: cp.applied == 1, - ...(cp.freeSpots && cp.totalSpots + visibility: this.getVisibility(cp), + ...(cp.freeSpots !== undefined && cp.totalSpots !== undefined ? { visibility: { freeSpots: cp.freeSpots, From 1227c37bdc0ed00a2d6ab31aaa86e8776d00cead Mon Sep 17 00:00:00 2001 From: "Davide Bizzi (aider)" Date: Thu, 1 Aug 2024 14:05:50 +0200 Subject: [PATCH 13/25] test: add unit tests for getVisibility functionality in visibility.spec.ts --- .../me/campaigns/_get/visibility.spec.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/routes/users/me/campaigns/_get/visibility.spec.ts diff --git a/src/routes/users/me/campaigns/_get/visibility.spec.ts b/src/routes/users/me/campaigns/_get/visibility.spec.ts new file mode 100644 index 000000000..826a723e1 --- /dev/null +++ b/src/routes/users/me/campaigns/_get/visibility.spec.ts @@ -0,0 +1,40 @@ +import { expect } from "chai"; +import RouteItem from "./index"; + +describe("getVisibility", () => { + const routeItem = new RouteItem({} as any); + + it("should return 'candidate' if the user is already applied", () => { + const result = routeItem["getVisibility"]({ + applied: true, + start_date: "2024-08-01T14:05:35.545180", + }); + expect(result).to.equal("candidate"); + }); + + it("should return 'unavailable' if the start date is in the future", () => { + const result = routeItem["getVisibility"]({ + applied: false, + start_date: "2999-08-01T14:05:35.545180", + }); + expect(result).to.equal("unavailable"); + }); + + it("should return 'unavailable' if there are no free spots", () => { + const result = routeItem["getVisibility"]({ + applied: false, + start_date: "2024-08-01T14:05:35.545180", + freeSpots: 0, + }); + expect(result).to.equal("unavailable"); + }); + + it("should return 'available' if the user is not applied, the start date is not in the future, and there are free spots", () => { + const result = routeItem["getVisibility"]({ + applied: false, + start_date: "2024-08-01T14:05:35.545180", + freeSpots: 5, + }); + expect(result).to.equal("available"); + }); +}); From e135f86d2a537741b7bd05abb59b0db14fc6ea90 Mon Sep 17 00:00:00 2001 From: "Davide Bizzi (aider)" Date: Thu, 1 Aug 2024 14:08:31 +0200 Subject: [PATCH 14/25] test: add tests for the `getVisibility` functionality in `visibility.spec.ts` using supertest and knex --- .../me/campaigns/_get/visibility.spec.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/routes/users/me/campaigns/_get/visibility.spec.ts b/src/routes/users/me/campaigns/_get/visibility.spec.ts index 826a723e1..39ddaa13e 100644 --- a/src/routes/users/me/campaigns/_get/visibility.spec.ts +++ b/src/routes/users/me/campaigns/_get/visibility.spec.ts @@ -38,3 +38,121 @@ describe("getVisibility", () => { expect(result).to.equal("available"); }); }); +import app from "@src/app"; +import request from "supertest"; +import { tryber } from "@src/features/database"; + +describe("GET /users/me/campaigns - visibility", () => { + beforeAll(async () => { + await tryber.tables.WpAppqEvdProfile.do().insert([ + { + id: 1, + wp_user_id: 1, + name: "jhon", + surname: "doe", + email: "jhon.doe@tryber.me", + employment_id: 1, + education_id: 1, + }, + ]); + await tryber.tables.WpUsers.do().insert([ + { + ID: 1, + user_login: "tester", + }, + ]); + const basicCampaignObject = { + start_date: new Date().toISOString().split("T")[0], + end_date: new Date().toISOString().split("T")[0], + close_date: new Date().toISOString().split("T")[0], + campaign_type_id: 1, + page_preview_id: 1, + page_manual_id: 2, + os: "1", + is_public: 0 as 0, + status_id: 1 as 1, + platform_id: 1, + customer_id: 1, + pm_id: 1, + project_id: 1, + customer_title: "Customer title", + phase_id: 20, + }; + await tryber.tables.WpAppqCampaignType.do().insert({ + id: 1, + name: "Type", + category_id: 1, + }); + await tryber.tables.WpAppqEvdCampaign.do().insert([ + { + ...basicCampaignObject, + id: 1, + title: "Campaign applied", + applied: 1, + }, + { + ...basicCampaignObject, + id: 2, + title: "Campaign future start date", + start_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], + }, + { + ...basicCampaignObject, + id: 3, + title: "Campaign no free spots", + freeSpots: 0, + }, + { + ...basicCampaignObject, + id: 4, + title: "Campaign available", + freeSpots: 5, + }, + ]); + }); + + afterAll(async () => { + await tryber.tables.WpAppqEvdCampaign.do().delete(); + await tryber.tables.WpAppqCampaignType.do().delete(); + await tryber.tables.WpUsers.do().delete(); + await tryber.tables.WpAppqEvdProfile.do().delete(); + }); + + it("should return 'candidate' if the user is already applied", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c) => c.id === 1); + expect(campaign.visibility).toBe("candidate"); + }); + + it("should return 'unavailable' if the start date is in the future", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c) => c.id === 2); + expect(campaign.visibility).toBe("unavailable"); + }); + + it("should return 'unavailable' if there are no free spots", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c) => c.id === 3); + expect(campaign.visibility).toBe("unavailable"); + }); + + it("should return 'available' if the user is not applied, the start date is not in the future, and there are free spots", async () => { + const response = await request(app) + .get("/users/me/campaigns") + .set("Authorization", "Bearer tester"); + expect(response.status).toBe(200); + const campaign = response.body.results.find((c) => c.id === 4); + expect(campaign.visibility).toBe("available"); + }); +}); From 642c6cca5eec117e65cc0c2d94fa8f6db82965fe Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 1 Aug 2024 16:14:12 +0200 Subject: [PATCH 15/25] feat: Add visiblity type --- .gitignore | 3 +- src/reference/openapi.yml | 8 ++ src/routes/users/me/campaigns/_get/index.ts | 18 +-- .../me/campaigns/_get/visibility.spec.ts | 106 ++++++++---------- src/schema.ts | 2 + 5 files changed, 69 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index 14c39f3af..750c476ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ coverage/ keys/* !/**/.gitkeep.idea .idea -.vscode/*.log \ No newline at end of file +.vscode/*.log +.aider* diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 2641dcc9f..898d08346 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -10414,6 +10414,14 @@ components: type: integer x-stoplight: id: 1jumwi4cvp91d + type: + type: string + x-stoplight: + id: o8jt4lts5ij18 + enum: + - available + - unavailable + - candidate CampaignRequired: description: '' type: object diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index 2d590be5f..f4d6f3dd6 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -124,15 +124,15 @@ class RouteItem extends UserRoute<{ manual_link: cp.manual_link, preview_link: cp.preview_link, applied: cp.applied == 1, - visibility: this.getVisibility(cp), - ...(cp.freeSpots !== undefined && cp.totalSpots !== undefined - ? { - visibility: { - freeSpots: cp.freeSpots, - totalSpots: cp.totalSpots, - }, - } - : {}), + visibility: { + type: this.getVisibility({ + applied: cp.applied == 1, + start_date: cp.start_date, + freeSpots: cp.freeSpots, + }), + ...(cp.freeSpots !== undefined ? { freeSpots: cp.freeSpots } : {}), + ...(cp.totalSpots !== undefined ? { totalSpots: cp.totalSpots } : {}), + }, })); } diff --git a/src/routes/users/me/campaigns/_get/visibility.spec.ts b/src/routes/users/me/campaigns/_get/visibility.spec.ts index 39ddaa13e..1b9c2702e 100644 --- a/src/routes/users/me/campaigns/_get/visibility.spec.ts +++ b/src/routes/users/me/campaigns/_get/visibility.spec.ts @@ -1,58 +1,35 @@ -import { expect } from "chai"; -import RouteItem from "./index"; - -describe("getVisibility", () => { - const routeItem = new RouteItem({} as any); - - it("should return 'candidate' if the user is already applied", () => { - const result = routeItem["getVisibility"]({ - applied: true, - start_date: "2024-08-01T14:05:35.545180", - }); - expect(result).to.equal("candidate"); - }); - - it("should return 'unavailable' if the start date is in the future", () => { - const result = routeItem["getVisibility"]({ - applied: false, - start_date: "2999-08-01T14:05:35.545180", - }); - expect(result).to.equal("unavailable"); - }); - - it("should return 'unavailable' if there are no free spots", () => { - const result = routeItem["getVisibility"]({ - applied: false, - start_date: "2024-08-01T14:05:35.545180", - freeSpots: 0, - }); - expect(result).to.equal("unavailable"); - }); - - it("should return 'available' if the user is not applied, the start date is not in the future, and there are free spots", () => { - const result = routeItem["getVisibility"]({ - applied: false, - start_date: "2024-08-01T14:05:35.545180", - freeSpots: 5, - }); - expect(result).to.equal("available"); - }); -}); import app from "@src/app"; -import request from "supertest"; import { tryber } from "@src/features/database"; +import resolvePermalinks from "@src/features/wp/resolvePermalinks"; +import request from "supertest"; +jest.mock("@src/features/wp/resolvePermalinks"); describe("GET /users/me/campaigns - visibility", () => { beforeAll(async () => { + (resolvePermalinks as jest.Mock).mockImplementation(() => { + return { + 1: { en: "en/test1", it: "it/test1", es: "es/test1" }, + 2: { en: "en/test2", it: "it/test2", es: "es/test2" }, + }; + }); + await tryber.seeds().campaign_statuses(); + const profile = { + name: "jhon", + surname: "doe", + email: "jhon.doe@tryber.me", + employment_id: 1, + education_id: 1, + }; await tryber.tables.WpAppqEvdProfile.do().insert([ { + ...profile, id: 1, wp_user_id: 1, - name: "jhon", - surname: "doe", - email: "jhon.doe@tryber.me", - employment_id: 1, - education_id: 1, + }, + { + ...profile, + id: 2, + wp_user_id: 2, }, ]); await tryber.tables.WpUsers.do().insert([ @@ -69,7 +46,7 @@ describe("GET /users/me/campaigns - visibility", () => { page_preview_id: 1, page_manual_id: 2, os: "1", - is_public: 0 as 0, + is_public: 1, status_id: 1 as 1, platform_id: 1, customer_id: 1, @@ -88,7 +65,6 @@ describe("GET /users/me/campaigns - visibility", () => { ...basicCampaignObject, id: 1, title: "Campaign applied", - applied: 1, }, { ...basicCampaignObject, @@ -102,13 +78,26 @@ describe("GET /users/me/campaigns - visibility", () => { ...basicCampaignObject, id: 3, title: "Campaign no free spots", - freeSpots: 0, + desired_number_of_testers: 1, + is_public: 4, }, { ...basicCampaignObject, id: 4, title: "Campaign available", - freeSpots: 5, + }, + ]); + + await tryber.tables.WpCrowdAppqHasCandidate.do().insert([ + { + campaign_id: 1, + user_id: 1, + accepted: 0, + }, + { + campaign_id: 3, + user_id: 2, + accepted: 0, }, ]); }); @@ -125,8 +114,9 @@ describe("GET /users/me/campaigns - visibility", () => { .get("/users/me/campaigns") .set("Authorization", "Bearer tester"); expect(response.status).toBe(200); - const campaign = response.body.results.find((c) => c.id === 1); - expect(campaign.visibility).toBe("candidate"); + console.log(response.body); + const campaign = response.body.results.find((c: any) => c.id === 1); + expect(campaign.visibility).toHaveProperty("type", "candidate"); }); it("should return 'unavailable' if the start date is in the future", async () => { @@ -134,8 +124,8 @@ describe("GET /users/me/campaigns - visibility", () => { .get("/users/me/campaigns") .set("Authorization", "Bearer tester"); expect(response.status).toBe(200); - const campaign = response.body.results.find((c) => c.id === 2); - expect(campaign.visibility).toBe("unavailable"); + const campaign = response.body.results.find((c: any) => c.id === 2); + expect(campaign.visibility).toHaveProperty("type", "unavailable"); }); it("should return 'unavailable' if there are no free spots", async () => { @@ -143,8 +133,8 @@ describe("GET /users/me/campaigns - visibility", () => { .get("/users/me/campaigns") .set("Authorization", "Bearer tester"); expect(response.status).toBe(200); - const campaign = response.body.results.find((c) => c.id === 3); - expect(campaign.visibility).toBe("unavailable"); + const campaign = response.body.results.find((c: any) => c.id === 3); + expect(campaign.visibility).toHaveProperty("type", "unavailable"); }); it("should return 'available' if the user is not applied, the start date is not in the future, and there are free spots", async () => { @@ -152,7 +142,7 @@ describe("GET /users/me/campaigns - visibility", () => { .get("/users/me/campaigns") .set("Authorization", "Bearer tester"); expect(response.status).toBe(200); - const campaign = response.body.results.find((c) => c.id === 4); - expect(campaign.visibility).toBe("available"); + const campaign = response.body.results.find((c: any) => c.id === 4); + expect(campaign.visibility).toHaveProperty("type", "available"); }); }); diff --git a/src/schema.ts b/src/schema.ts index 69191d782..3c2a34e47 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -701,6 +701,8 @@ export interface components { visibility?: { freeSpots?: number; totalSpots?: number; + /** @enum {string} */ + type?: "available" | "unavailable" | "candidate"; }; }; CampaignRequired: { From b55c7dae3eb35c99f4c32e3d5092ff7cb26ff09c Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 1 Aug 2024 16:29:31 +0200 Subject: [PATCH 16/25] feat: Allow order by visibility --- src/reference/openapi.yml | 1 + src/routes/users/me/campaigns/_get/index.ts | 88 ++++++++++++++++++--- src/schema.ts | 7 +- 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 898d08346..708a13047 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -6346,6 +6346,7 @@ paths: - start_date - end_date - close_date + - visibility in: query name: orderBy description: The field for item order diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index f4d6f3dd6..2381e3b82 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -17,7 +17,12 @@ class RouteItem extends UserRoute<{ completed?: "0" | "1"; statusId?: "1" | "2"; } = {}; - private orderBy: "start_date" | "end_date" | "close_date" | undefined; + private orderBy: + | "start_date" + | "end_date" + | "close_date" + | "visibility" + | undefined; private order: NonNullable["order"] | undefined; private start: NonNullable["start"]> = 0; @@ -50,6 +55,7 @@ class RouteItem extends UserRoute<{ if (orderBy === "start_date") this.orderBy = "start_date"; if (orderBy === "end_date") this.orderBy = "end_date"; if (orderBy === "close_date") this.orderBy = "close_date"; + if (orderBy === "visibility") this.orderBy = "visibility"; } private setOrder() { @@ -80,9 +86,9 @@ class RouteItem extends UserRoute<{ } private async getCampaigns() { - const results = await this.getCampaignsQuery(); + const campaigns = await this.getCampaignsQuery(); - if (!results.length) { + if (!campaigns.length) { throw Error("no data found"); } @@ -91,7 +97,7 @@ class RouteItem extends UserRoute<{ await this.enhanceWithTargetRules( await this.enhanceWithLinkedPages( await this.enhanceWithCampaignType( - await this.enhanceCampaignsWithApplication(results) + await this.enhanceCampaignsWithApplication(campaigns) ) ) ) @@ -102,7 +108,7 @@ class RouteItem extends UserRoute<{ throw Error("no data found"); } - return items + const results = items .filter((campaign) => { if (this.filterByAccepted()) return campaign.accepted; else @@ -134,6 +140,65 @@ class RouteItem extends UserRoute<{ ...(cp.totalSpots !== undefined ? { totalSpots: cp.totalSpots } : {}), }, })); + + if (this.orderBy === "visibility") { + results.sort((a, b) => { + const { + type: typeA, + freeSpots: freeSpotsA, + totalSpots: totalSpotsA, + } = a.visibility; + const { + type: typeB, + freeSpots: freeSpotsB, + totalSpots: totalSpotsB, + } = b.visibility; + const startA = new Date(a.dates.start); + const startB = new Date(b.dates.start); + + // Helper function to calculate ratio + const ratio = (freeSpots?: number, totalSpots?: number) => + freeSpots !== undefined && totalSpots !== undefined + ? freeSpots / totalSpots + : Infinity; + + if (typeA === "available" && typeB === "available") { + const hasSpotsA = + freeSpotsA !== undefined && totalSpotsA !== undefined; + const hasSpotsB = + freeSpotsB !== undefined && totalSpotsB !== undefined; + + if (hasSpotsA && hasSpotsB) { + // Both have spots: sort by ratio + return ( + ratio(freeSpotsA, totalSpotsA) - ratio(freeSpotsB, totalSpotsB) + ); + } else if (hasSpotsA && !hasSpotsB) { + // A has spots, B doesn't + return -1; + } else if (!hasSpotsA && hasSpotsB) { + // B has spots, A doesn't + return 1; + } else { + // Neither have spots: sort by start date + return startA.getTime() - startB.getTime(); + } + } else if (typeA === "available" && typeB !== "available") { + return -1; // "available" campaigns come first + } else if (typeA !== "available" && typeB === "available") { + return 1; // "available" campaigns come first + } else if (typeA === "candidate" && typeB === "unavailable") { + return -1; // "candidate" comes before "unavailable" + } else if (typeA === "unavailable" && typeB === "candidate") { + return 1; // "candidate" comes before "unavailable" + } else { + // Both are "unavailable" or "candidate": sort by start date + return startA.getTime() - startB.getTime(); + } + }); + } + + return results; } private async getCampaignsQuery() { @@ -196,10 +261,15 @@ class RouteItem extends UserRoute<{ query.where("status_id", 1); } - query.orderBy( - this.orderBy || "wp_appq_evd_campaign.id", - this.orderBy ? this.order || "DESC" : "ASC" - ); + if (this.orderBy) { + if (this.orderBy === "visibility") { + query.orderBy("start_date", this.order || "DESC"); + } else { + query.orderBy(this.orderBy, this.order || "DESC"); + } + } else { + query.orderBy("wp_appq_evd_campaign.id", "ASC"); + } return query; } diff --git a/src/schema.ts b/src/schema.ts index 3c2a34e47..237cd683a 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -3097,7 +3097,12 @@ export interface operations { /** How to order values (ASC, DESC) */ order?: components["parameters"]["order"]; /** The field for item order */ - orderBy?: "name" | "start_date" | "end_date" | "close_date"; + orderBy?: + | "name" + | "start_date" + | "end_date" + | "close_date" + | "visibility"; }; }; responses: { From e964cd78a222eabae5a54b7eb7cc86038b9ca0bf Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 2 Aug 2024 10:45:23 +0200 Subject: [PATCH 17/25] fix: Set unavailable when start date is past --- src/routes/users/me/campaigns/_get/index.ts | 2 +- src/routes/users/me/campaigns/_get/visibility.spec.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index 2381e3b82..658117f12 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -44,7 +44,7 @@ class RouteItem extends UserRoute<{ freeSpots?: number; }): "candidate" | "unavailable" | "available" { if (campaign.applied) return "candidate"; - if (new Date(campaign.start_date) > new Date() || campaign.freeSpots === 0) + if (new Date(campaign.start_date) <= new Date() || campaign.freeSpots === 0) return "unavailable"; return "available"; } diff --git a/src/routes/users/me/campaigns/_get/visibility.spec.ts b/src/routes/users/me/campaigns/_get/visibility.spec.ts index 1b9c2702e..0e1aba634 100644 --- a/src/routes/users/me/campaigns/_get/visibility.spec.ts +++ b/src/routes/users/me/campaigns/_get/visibility.spec.ts @@ -69,8 +69,8 @@ describe("GET /users/me/campaigns - visibility", () => { { ...basicCampaignObject, id: 2, - title: "Campaign future start date", - start_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + title: "Campaign past start date", + start_date: new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000) .toISOString() .split("T")[0], }, @@ -84,7 +84,10 @@ describe("GET /users/me/campaigns - visibility", () => { { ...basicCampaignObject, id: 4, - title: "Campaign available", + title: "Campaign future start date", + start_date: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], }, ]); From f4e69e24a53bfdac17dbb92a96490b4669507064 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Fri, 2 Aug 2024 10:47:35 +0200 Subject: [PATCH 18/25] feat: Don't show spots if cap -1 --- src/routes/users/me/campaigns/_get/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/users/me/campaigns/_get/index.ts b/src/routes/users/me/campaigns/_get/index.ts index 658117f12..bd5b997d7 100644 --- a/src/routes/users/me/campaigns/_get/index.ts +++ b/src/routes/users/me/campaigns/_get/index.ts @@ -454,7 +454,7 @@ class RouteItem extends UserRoute<{ ); return { ...campaign, - ...(applicationSpot + ...(applicationSpot && applicationSpot.cap >= 0 ? { freeSpots: applicationSpot.cap - (validApplicationsCount?.count || 0), From c87340882b7ef22fb89721381eaf57ec8dc44922 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 5 Aug 2024 12:49:07 +0200 Subject: [PATCH 19/25] feat: Change format for invalid options in get --- src/reference/openapi.yml | 56 ++++++++++--------- .../campaigns/forms/formId/_get/index.spec.ts | 19 ++++--- .../campaigns/forms/formId/_get/index.ts | 21 ++++--- .../forms/formId/_get/screenout.spec.ts | 14 +++-- src/schema.ts | 18 +++--- 5 files changed, 73 insertions(+), 55 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 708a13047..ffec1a6cf 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -10640,8 +10640,13 @@ components: - properties: type: type: string + x-stoplight: + id: hio24fhxketl6 enum: - text + - gender + - phone_number + - address required: - type - properties: @@ -10654,15 +10659,18 @@ components: options: type: array items: - type: string - invalidOptions: - type: array - x-stoplight: - id: xgjgbndwz4cd3 - items: - x-stoplight: - id: hdc1yjxxy6con - type: string + type: object + properties: + value: + type: string + x-stoplight: + id: bqwax1jmhhsc5 + isInvalid: + type: boolean + x-stoplight: + id: 8xwnpgdiarx8k + required: + - value required: - type - options @@ -10673,24 +10681,18 @@ components: options: type: array items: - type: integer - invalidOptions: - type: array - x-stoplight: - id: ytwf24veu2syh - items: - x-stoplight: - id: adavsvppix2u2 - type: integer - required: - - type - - properties: - type: - type: string - enum: - - gender - - phone_number - - address + type: object + properties: + value: + type: number + x-stoplight: + id: pkxj2bvb8vuj3 + isInvalid: + type: boolean + x-stoplight: + id: 7sffsmb0sefc9 + required: + - value required: - type type: object diff --git a/src/routes/campaigns/forms/formId/_get/index.spec.ts b/src/routes/campaigns/forms/formId/_get/index.spec.ts index dc675fc34..5a35aaad3 100644 --- a/src/routes/campaigns/forms/formId/_get/index.spec.ts +++ b/src/routes/campaigns/forms/formId/_get/index.spec.ts @@ -1,8 +1,8 @@ -import app from "@src/app"; -import request from "supertest"; +import Campaign from "@src/__mocks__/mockedDb/campaign"; import PreselectionForm from "@src/__mocks__/mockedDb/preselectionForm"; import PreselectionFormFields from "@src/__mocks__/mockedDb/preselectionFormFields"; -import Campaign from "@src/__mocks__/mockedDb/campaign"; +import app from "@src/app"; +import request from "supertest"; describe("GET /campaigns/forms/{formId}", () => { beforeAll(() => { @@ -132,23 +132,28 @@ describe("GET /campaigns/forms/{formId}", () => { ); expect(response.body).toHaveProperty("fields"); expect(response.body.fields).toEqual([ - { id: 8, type: "cuf_1", options: [1, 2, 3], question: "Cuf question" }, + { + id: 8, + type: "cuf_1", + options: [{ value: 1 }, { value: 2 }, { value: 3 }], + question: "Cuf question", + }, { id: 2, type: "select", - options: ["Option 1", "Option 2"], + options: [{ value: "Option 1" }, { value: "Option 2" }], question: "Select question", }, { id: 3, type: "multiselect", - options: ["Option 3", "Option 4"], + options: [{ value: "Option 3" }, { value: "Option 4" }], question: "Multiselect question", }, { id: 4, type: "radio", - options: ["Yes", "No"], + options: [{ value: "Yes" }, { value: "No" }], question: "Radio question", }, { id: 5, type: "gender", question: "Gender question" }, diff --git a/src/routes/campaigns/forms/formId/_get/index.ts b/src/routes/campaigns/forms/formId/_get/index.ts index 9f0e2f88d..199761130 100644 --- a/src/routes/campaigns/forms/formId/_get/index.ts +++ b/src/routes/campaigns/forms/formId/_get/index.ts @@ -101,19 +101,24 @@ export default class RouteItem extends UserRoute<{ .orderBy("priority", "asc"); return results.map((item) => { + const options = + isFieldTypeWithOptions(item.type) && item.options + ? parseOptions(item.options) + : undefined; + return { id: item.id, type: item.type, question: item.question, short_name: item.short_name ? item.short_name : undefined, - options: - isFieldTypeWithOptions(item.type) && item.options - ? parseOptions(item.options) - : undefined, - invalidOptions: - isFieldTypeWithOptions(item.type) && item.invalid_options - ? parseOptions(item.invalid_options) - : undefined, + options: options + ? options.map((value: string | number) => { + const isInvalid = ( + item.invalid_options ? parseOptions(item.invalid_options) : [] + ).includes(value); + return { value, ...(isInvalid ? { isInvalid } : {}) }; + }) + : undefined, }; }); diff --git a/src/routes/campaigns/forms/formId/_get/screenout.spec.ts b/src/routes/campaigns/forms/formId/_get/screenout.spec.ts index 2554d4883..7bf5ad835 100644 --- a/src/routes/campaigns/forms/formId/_get/screenout.spec.ts +++ b/src/routes/campaigns/forms/formId/_get/screenout.spec.ts @@ -80,7 +80,10 @@ describe("GET /campaigns/forms/{formId} - screenout data", () => { expect.arrayContaining([ expect.objectContaining({ id: 2, - invalidOptions: ["Option 1"], + options: [ + { value: "Option 1", isInvalid: true }, + { value: "Option 2" }, + ], }), ]) ); @@ -98,7 +101,10 @@ describe("GET /campaigns/forms/{formId} - screenout data", () => { expect.arrayContaining([ expect.objectContaining({ id: 3, - invalidOptions: ["Option 3"], + options: [ + { value: "Option 3", isInvalid: true }, + { value: "Option 4" }, + ], }), ]) ); @@ -116,7 +122,7 @@ describe("GET /campaigns/forms/{formId} - screenout data", () => { expect.arrayContaining([ expect.objectContaining({ id: 4, - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }), ]) ); @@ -134,7 +140,7 @@ describe("GET /campaigns/forms/{formId} - screenout data", () => { expect.arrayContaining([ expect.objectContaining({ id: 8, - invalidOptions: [1], + options: [{ value: 1, isInvalid: true }, { value: 2 }, { value: 3 }], }), ]) ); diff --git a/src/schema.ts b/src/schema.ts index 237cd683a..0f1d171a4 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -802,22 +802,22 @@ export interface components { } & ( | { /** @enum {string} */ - type: "text"; + type: "text" | "gender" | "phone_number" | "address"; } | { /** @enum {string} */ type: "multiselect" | "select" | "radio"; - options: string[]; - invalidOptions?: string[]; + options: { + value: string; + isInvalid: boolean; + }[]; } | { type: string; - options?: number[]; - invalidOptions?: number[]; - } - | { - /** @enum {string} */ - type: "gender" | "phone_number" | "address"; + options?: { + value: number; + isInvalid: boolean; + }[]; } ); /** Project */ From bc2d8515f20a2dc5785286be9ac917d8ef708427 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 5 Aug 2024 14:42:49 +0200 Subject: [PATCH 20/25] feat: Change format for screenout options in post --- src/routes/campaigns/forms/FieldCreator.ts | 4 +-- .../campaigns/forms/_post/index.spec.ts | 2 +- src/routes/campaigns/forms/_post/index.ts | 19 +++++++--- .../campaigns/forms/_post/screenout.spec.ts | 36 ++++++++++++------- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/routes/campaigns/forms/FieldCreator.ts b/src/routes/campaigns/forms/FieldCreator.ts index c0045230c..d055c7c3c 100644 --- a/src/routes/campaigns/forms/FieldCreator.ts +++ b/src/routes/campaigns/forms/FieldCreator.ts @@ -32,8 +32,8 @@ export default class FieldCreator { question: string; short_name?: string; type: string; - options?: string[] | number[]; - invalid_options?: string[] | number[]; + options?: (string | number)[]; + invalid_options?: (string | number)[]; id?: number; priority: number; }) { diff --git a/src/routes/campaigns/forms/_post/index.spec.ts b/src/routes/campaigns/forms/_post/index.spec.ts index 9047aed3f..833ef5b20 100644 --- a/src/routes/campaigns/forms/_post/index.spec.ts +++ b/src/routes/campaigns/forms/_post/index.spec.ts @@ -416,7 +416,7 @@ async function checkValidFieldWithOptions({ { question, type, - options, + options: options.map((value) => ({ value })), }, ], }; diff --git a/src/routes/campaigns/forms/_post/index.ts b/src/routes/campaigns/forms/_post/index.ts index 66fc7815b..9d9e1ac3b 100644 --- a/src/routes/campaigns/forms/_post/index.ts +++ b/src/routes/campaigns/forms/_post/index.ts @@ -47,7 +47,7 @@ export default class RouteItem extends UserRoute<{ const fields = await this.createFields(form.id); this.setSuccess(201, { ...form, - fields, + fields: fields, }); } catch (e) { const error = e as OpenapiError; @@ -133,13 +133,24 @@ export default class RouteItem extends UserRoute<{ question: field.question, short_name: field.short_name, type: field.type, - options: "options" in field ? field.options : undefined, + options: + "options" in field && field.options + ? field.options.map((o) => o.value) + : undefined, invalid_options: - "invalidOptions" in field ? field.invalidOptions : undefined, + "options" in field && field.options + ? field.options.filter((o) => o.isInvalid).map((o) => o.value) + : undefined, priority: i++, }); try { - results.push(await item.create()); + const result = await item.create(); + results.push({ + ...result, + options: result.options + ? result.options.map((o: number | string) => ({ value: o })) + : undefined, + }); } catch (e) { throw { status_code: 406, diff --git a/src/routes/campaigns/forms/_post/screenout.spec.ts b/src/routes/campaigns/forms/_post/screenout.spec.ts index daf2820ed..2de5356a6 100644 --- a/src/routes/campaigns/forms/_post/screenout.spec.ts +++ b/src/routes/campaigns/forms/_post/screenout.spec.ts @@ -77,8 +77,7 @@ describe("POST /campaigns/forms/ - screenout", () => { question: "Yes or no", type: "select", priority: 1, - options: ["Yes", "No"], - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }, ], creationDate: "2024-02-23 00:00:00", @@ -101,7 +100,7 @@ describe("POST /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Yes or no", - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }); }); it("Should save screenout options for multiselect", async () => { @@ -114,8 +113,11 @@ describe("POST /campaigns/forms/ - screenout", () => { question: "Select one", type: "multiselect", priority: 1, - options: ["Blue", "Red", "Yellow"], - invalidOptions: ["Blue", "Red"], + options: [ + { value: "Blue", isInvalid: true }, + { value: "Red", isInvalid: true }, + { value: "Yellow" }, + ], }, ], creationDate: "2024-02-23 00:00:00", @@ -138,7 +140,11 @@ describe("POST /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Select one", - invalidOptions: ["Blue", "Red"], + options: [ + { value: "Blue", isInvalid: true }, + { value: "Red", isInvalid: true }, + { value: "Yellow" }, + ], }); }); it("Should save screenout options for radio", async () => { @@ -151,8 +157,7 @@ describe("POST /campaigns/forms/ - screenout", () => { question: "Yes or no", type: "radio", priority: 1, - options: ["Yes", "No"], - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }, ], creationDate: "2024-02-23 00:00:00", @@ -175,7 +180,7 @@ describe("POST /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Yes or no", - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }); }); it("Should save screenout options for cuf", async () => { @@ -188,8 +193,11 @@ describe("POST /campaigns/forms/ - screenout", () => { question: "Electricity", type: "cuf_1", priority: 1, - options: [1, 2, 3], - invalidOptions: [1, 2], + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], }, ], creationDate: "2024-02-23 00:00:00", @@ -212,7 +220,11 @@ describe("POST /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Electricity", - invalidOptions: [1, 2], + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], }); }); }); From 0f2471798d0f9420a0a8b9c6d0f3927cde99064a Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 5 Aug 2024 14:48:16 +0200 Subject: [PATCH 21/25] feat: Change format for invalid options in put --- .../campaigns/forms/formId/_put/index.spec.ts | 10 +++--- .../campaigns/forms/formId/_put/index.ts | 9 ++++- .../forms/formId/_put/screenout.spec.ts | 36 ++++++++++++------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/routes/campaigns/forms/formId/_put/index.spec.ts b/src/routes/campaigns/forms/formId/_put/index.spec.ts index ca269bf6f..eedb91207 100644 --- a/src/routes/campaigns/forms/formId/_put/index.spec.ts +++ b/src/routes/campaigns/forms/formId/_put/index.spec.ts @@ -1,8 +1,8 @@ -import app from "@src/app"; -import request from "supertest"; -import { tryber } from "@src/features/database"; import PreselectionForm from "@src/__mocks__/mockedDb/preselectionForm"; import PreselectionFormFields from "@src/__mocks__/mockedDb/preselectionFormFields"; +import app from "@src/app"; +import { tryber } from "@src/features/database"; +import request from "supertest"; const basicCampaign = { title: "Test Campaign", @@ -47,13 +47,13 @@ const sampleBodyWithFields = { { id: 3, type: "cuf_2", - options: [1, 2], + options: [{ value: 1 }, { value: 2 }], question: "My select cuf question", }, { id: 4, type: "select", - options: ["option1", "option2"], + options: [{ value: "option1" }, { value: "option2" }], question: "My select question", }, ], diff --git a/src/routes/campaigns/forms/formId/_put/index.ts b/src/routes/campaigns/forms/formId/_put/index.ts index e0559e55b..c542cfffb 100644 --- a/src/routes/campaigns/forms/formId/_put/index.ts +++ b/src/routes/campaigns/forms/formId/_put/index.ts @@ -123,8 +123,15 @@ export default class RouteItem extends UserRoute<{ const fieldCreator = new FieldCreator({ ...field, formId: this.getId(), + + options: + "options" in field && field.options + ? field.options.map((o) => o.value) + : undefined, invalid_options: - "invalidOptions" in field ? field.invalidOptions : undefined, + "options" in field && field.options + ? field.options.filter((o) => o.isInvalid).map((o) => o.value) + : undefined, priority: i++, }); await fieldCreator.create(); diff --git a/src/routes/campaigns/forms/formId/_put/screenout.spec.ts b/src/routes/campaigns/forms/formId/_put/screenout.spec.ts index 5b84d2dc2..8f2193124 100644 --- a/src/routes/campaigns/forms/formId/_put/screenout.spec.ts +++ b/src/routes/campaigns/forms/formId/_put/screenout.spec.ts @@ -156,8 +156,7 @@ describe("PUT /campaigns/forms/ - screenout", () => { { question: "Yes or no", type: "select", - options: ["Yes", "No"], - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }, ], }) @@ -178,7 +177,7 @@ describe("PUT /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Yes or no", - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }); }); @@ -191,8 +190,11 @@ describe("PUT /campaigns/forms/ - screenout", () => { { question: "Select one", type: "multiselect", - options: ["Red", "Blue", "Yellow"], - invalidOptions: ["Red", "Blue"], + options: [ + { value: "Red", isInvalid: true }, + { value: "Blue", isInvalid: true }, + { value: "Yellow" }, + ], }, ], }) @@ -213,7 +215,11 @@ describe("PUT /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Select one", - invalidOptions: ["Red", "Blue"], + options: [ + { value: "Red", isInvalid: true }, + { value: "Blue", isInvalid: true }, + { value: "Yellow" }, + ], }); }); @@ -226,8 +232,7 @@ describe("PUT /campaigns/forms/ - screenout", () => { { question: "Yes or no", type: "radio", - options: ["Yes", "No"], - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }, ], }) @@ -248,7 +253,7 @@ describe("PUT /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Yes or no", - invalidOptions: ["No"], + options: [{ value: "Yes" }, { value: "No", isInvalid: true }], }); }); @@ -261,8 +266,11 @@ describe("PUT /campaigns/forms/ - screenout", () => { { question: "Electricity", type: "cuf_1", - options: [1, 2, 3], - invalidOptions: [1, 2], + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], }, ], }) @@ -283,7 +291,11 @@ describe("PUT /campaigns/forms/ - screenout", () => { expect(get.body.fields).toHaveLength(1); expect(get.body.fields[0]).toMatchObject({ question: "Electricity", - invalidOptions: [1, 2], + options: [ + { value: 1, isInvalid: true }, + { value: 2, isInvalid: true }, + { value: 3 }, + ], }); }); }); From 2af173ec83ce193832b9a42d1b4da85a661d3889 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 5 Aug 2024 15:02:30 +0200 Subject: [PATCH 22/25] chore: Fix typescript issue --- src/routes/campaigns/forms/_post/index.ts | 15 +++++++++++---- src/routes/campaigns/forms/formId/_put/index.ts | 11 +++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/routes/campaigns/forms/_post/index.ts b/src/routes/campaigns/forms/_post/index.ts index 9d9e1ac3b..bf6cba786 100644 --- a/src/routes/campaigns/forms/_post/index.ts +++ b/src/routes/campaigns/forms/_post/index.ts @@ -128,6 +128,10 @@ export default class RouteItem extends UserRoute<{ const results = []; let i = 1; for (const field of body.fields) { + const options = + "options" in field && field.options + ? (field.options as { isInvalid: boolean; value: string | number }[]) + : undefined; const item = new FieldCreator({ formId: formId, question: field.question, @@ -137,10 +141,13 @@ export default class RouteItem extends UserRoute<{ "options" in field && field.options ? field.options.map((o) => o.value) : undefined, - invalid_options: - "options" in field && field.options - ? field.options.filter((o) => o.isInvalid).map((o) => o.value) - : undefined, + invalid_options: options + ? options + .filter( + (o): o is { isInvalid: true; value: number } => o.isInvalid + ) + .map((o) => o.value) + : undefined, priority: i++, }); try { diff --git a/src/routes/campaigns/forms/formId/_put/index.ts b/src/routes/campaigns/forms/formId/_put/index.ts index c542cfffb..6714b2977 100644 --- a/src/routes/campaigns/forms/formId/_put/index.ts +++ b/src/routes/campaigns/forms/formId/_put/index.ts @@ -120,6 +120,10 @@ export default class RouteItem extends UserRoute<{ await this.clearFields(); let i = 1; for (const field of fields) { + const options = + "options" in field && field.options + ? (field.options as { isInvalid: boolean; value: string | number }[]) + : undefined; const fieldCreator = new FieldCreator({ ...field, formId: this.getId(), @@ -128,10 +132,9 @@ export default class RouteItem extends UserRoute<{ "options" in field && field.options ? field.options.map((o) => o.value) : undefined, - invalid_options: - "options" in field && field.options - ? field.options.filter((o) => o.isInvalid).map((o) => o.value) - : undefined, + invalid_options: options + ? options.filter((o) => o.isInvalid).map((o) => o.value) + : undefined, priority: i++, }); await fieldCreator.create(); From 2bd10bb81d7ebd2e54750b7af3fb934e4b2769fc Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 7 Aug 2024 12:47:41 +0200 Subject: [PATCH 23/25] fix: Split preselection form type in tester/admin facing --- src/reference/openapi.yml | 117 ++++++++++++++---- .../campaigns/forms/_post/screenout.spec.ts | 1 + src/schema.ts | 50 ++++++-- 3 files changed, 128 insertions(+), 40 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index ffec1a6cf..7dadf4835 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -7031,9 +7031,15 @@ paths: type: array items: allOf: - - $ref: '#/components/schemas/PreselectionFormQuestion' - - type: object - properties: + - properties: + question: + type: string + x-stoplight: + id: ir6xgrpexauf5 + short_name: + type: string + x-stoplight: + id: bl9s0k131fjp4 value: oneOf: - type: integer @@ -7059,7 +7065,48 @@ paths: id: type: number required: + - question - id + - x-stoplight: + id: 76ya1n62vrxoj + oneOf: + - properties: + type: + $ref: '#/components/schemas/PreselectionQuestionSimple' + required: + - type + - x-stoplight: + id: age6qie67x6dt + properties: + type: + $ref: '#/components/schemas/PreselectionQuestionMultiple' + options: + type: array + x-stoplight: + id: 3eqfw8hp0j217 + items: + x-stoplight: + id: u67gsrnn8bhvb + type: string + required: + - type + - options + - x-stoplight: + id: v6nzvavd1bbnh + properties: + type: + $ref: '#/components/schemas/PreselectionQuestionCuf' + options: + type: array + x-stoplight: + id: ilyb6ci3tao5c + items: + x-stoplight: + id: 511phg4idvt3g + type: number + required: + - type + type: object examples: example-1: value: @@ -10639,60 +10686,51 @@ components: - oneOf: - properties: type: - type: string - x-stoplight: - id: hio24fhxketl6 - enum: - - text - - gender - - phone_number - - address + $ref: '#/components/schemas/PreselectionQuestionSimple' required: - type - properties: type: - type: string - enum: - - multiselect - - select - - radio + $ref: '#/components/schemas/PreselectionQuestionMultiple' options: type: array + x-stoplight: + id: xdbut0hjezieh items: + x-stoplight: + id: cln0chyj7s107 type: object properties: value: type: string x-stoplight: - id: bqwax1jmhhsc5 + id: ifhnncm4ap0l8 isInvalid: type: boolean x-stoplight: - id: 8xwnpgdiarx8k - required: - - value + id: 6imsh5mbep30d required: - type - - options - properties: type: - type: string - pattern: '^cuf_[0-9]*$' + $ref: '#/components/schemas/PreselectionQuestionCuf' options: type: array + x-stoplight: + id: 98p0edyoy9t49 items: + x-stoplight: + id: lri0oems0d8by type: object properties: value: type: number x-stoplight: - id: pkxj2bvb8vuj3 + id: hrwvvg2r2jm4v isInvalid: type: boolean x-stoplight: - id: 7sffsmb0sefc9 - required: - - value + id: wiocd6n2pq0z5 required: - type type: object @@ -11052,6 +11090,31 @@ components: - title - startDate - deviceList + PreselectionQuestionSimple: + title: PreselectionQuestionSimple + x-stoplight: + id: huxuwdbvi0pep + type: string + enum: + - gender + - text + - phone_number + - address + PreselectionQuestionMultiple: + title: PreselectionQuestionMultiple + x-stoplight: + id: op7khbr8grgql + type: string + enum: + - multiselect + - select + - radio + PreselectionQuestionCuf: + title: PreselectionQuestionCuf + x-stoplight: + id: 5diemuprwjolx + type: string + pattern: '^cuf_[0-9]*$' securitySchemes: JWT: type: http diff --git a/src/routes/campaigns/forms/_post/screenout.spec.ts b/src/routes/campaigns/forms/_post/screenout.spec.ts index 2de5356a6..e22cb60f0 100644 --- a/src/routes/campaigns/forms/_post/screenout.spec.ts +++ b/src/routes/campaigns/forms/_post/screenout.spec.ts @@ -206,6 +206,7 @@ describe("POST /campaigns/forms/ - screenout", () => { "authorization", `Bearer tester capability ["manage_preselection_forms"]` ); + console.log(response.body); expect(response.status).toBe(201); const { id } = response.body; diff --git a/src/schema.ts b/src/schema.ts index 0f1d171a4..8db6aeba2 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -801,23 +801,21 @@ export interface components { short_name?: string; } & ( | { - /** @enum {string} */ - type: "text" | "gender" | "phone_number" | "address"; + type: components["schemas"]["PreselectionQuestionSimple"]; } | { - /** @enum {string} */ - type: "multiselect" | "select" | "radio"; - options: { - value: string; - isInvalid: boolean; + type: components["schemas"]["PreselectionQuestionMultiple"]; + options?: { + value?: string; + isInvalid?: boolean; }[]; } | { - type: string; + type: components["schemas"]["PreselectionQuestionCuf"]; options?: { - value: number; - isInvalid: boolean; - }[]; + value?: number; + isInvalid?: boolean; + }; } ); /** Project */ @@ -943,6 +941,18 @@ export interface components { productType?: number; notes?: string; }; + /** + * PreselectionQuestionSimple + * @enum {string} + */ + PreselectionQuestionSimple: "gender" | "text" | "phone_number" | "address"; + /** + * PreselectionQuestionMultiple + * @enum {string} + */ + PreselectionQuestionMultiple: "multiselect" | "select" | "radio"; + /** PreselectionQuestionCuf */ + PreselectionQuestionCuf: string; }; responses: { /** A user */ @@ -3301,7 +3311,9 @@ export interface operations { /** OK */ 200: { content: { - "application/json": (components["schemas"]["PreselectionFormQuestion"] & { + "application/json": ({ + question: string; + short_name?: string; value?: | number | { @@ -3315,7 +3327,19 @@ export interface operations { error?: string; }; id: number; - })[]; + } & ( + | { + type: components["schemas"]["PreselectionQuestionSimple"]; + } + | { + type: components["schemas"]["PreselectionQuestionMultiple"]; + options: string[]; + } + | { + type: components["schemas"]["PreselectionQuestionCuf"]; + options?: number[]; + } + ))[]; }; }; 403: components["responses"]["NotAuthorized"]; From 3ddfd16f1e45200536749e1d03e6efdecb8eb030 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Wed, 7 Aug 2024 12:49:44 +0200 Subject: [PATCH 24/25] chore: Fix required fields --- src/reference/openapi.yml | 4 ++++ src/schema.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/reference/openapi.yml b/src/reference/openapi.yml index 7dadf4835..89cbbea7a 100644 --- a/src/reference/openapi.yml +++ b/src/reference/openapi.yml @@ -10709,6 +10709,8 @@ components: type: boolean x-stoplight: id: 6imsh5mbep30d + required: + - value required: - type - properties: @@ -10731,6 +10733,8 @@ components: type: boolean x-stoplight: id: wiocd6n2pq0z5 + required: + - value required: - type type: object diff --git a/src/schema.ts b/src/schema.ts index 8db6aeba2..005028ec7 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -806,16 +806,16 @@ export interface components { | { type: components["schemas"]["PreselectionQuestionMultiple"]; options?: { - value?: string; + value: string; isInvalid?: boolean; }[]; } | { type: components["schemas"]["PreselectionQuestionCuf"]; options?: { - value?: number; + value: number; isInvalid?: boolean; - }; + }[]; } ); /** Project */ From ed9fb3c2ba35be2d113a7d298d8afb548af5d6da Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 8 Aug 2024 15:06:47 +0200 Subject: [PATCH 25/25] fix: Ignore empty target rules --- .../users/me/campaigns/_get/UserTargetChecker.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/routes/users/me/campaigns/_get/UserTargetChecker.ts b/src/routes/users/me/campaigns/_get/UserTargetChecker.ts index e27c7fe21..e11e5e896 100644 --- a/src/routes/users/me/campaigns/_get/UserTargetChecker.ts +++ b/src/routes/users/me/campaigns/_get/UserTargetChecker.ts @@ -38,11 +38,19 @@ export class UserTargetChecker { if (Object.keys(targetRules).length === 0) return true; const { languages, countries } = targetRules; - if (languages && !languages.some((l) => this.userLanguages.includes(l))) { + if ( + languages && + languages.length && + !languages.some((l) => this.userLanguages.includes(l)) + ) { return false; } - if (countries && !countries.includes(this.userCountry)) { + if ( + countries && + countries.length && + !countries.includes(this.userCountry) + ) { return false; }