From 44aa0cd54132213400d8625daf1cdf25ee975b58 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Thu, 21 Apr 2022 21:28:34 +0200 Subject: [PATCH 1/7] feat: adds webshot service to publish projects --- ...9314086-AddLocationPngDataPublicProject.ts | 20 ++++++++++ .../dto/create-published-project.dto.ts | 2 + .../dto/publish-project.dto.ts | 13 ++++++- .../entities/published-project.api.entity.ts | 7 ++++ .../published-project.module.ts | 4 +- .../published-project.service.ts | 38 ++++++++++++++++--- api/libs/webshot/src/webshot.service.ts | 27 +++++++++++++ 7 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 api/apps/api/src/migrations/api/1650619314086-AddLocationPngDataPublicProject.ts diff --git a/api/apps/api/src/migrations/api/1650619314086-AddLocationPngDataPublicProject.ts b/api/apps/api/src/migrations/api/1650619314086-AddLocationPngDataPublicProject.ts new file mode 100644 index 0000000000..8bb8045b34 --- /dev/null +++ b/api/apps/api/src/migrations/api/1650619314086-AddLocationPngDataPublicProject.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddLocationsPngDataPublicProject1650619314086 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE published_projects + ADD COLUMN location varchar, + ADD COLUMN png_data text; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE published_projects + DROP COLUMN location, + DROP COLUMN png_data; + `); + } +} diff --git a/api/apps/api/src/modules/published-project/dto/create-published-project.dto.ts b/api/apps/api/src/modules/published-project/dto/create-published-project.dto.ts index 2da3f5c0b1..5bca676c04 100644 --- a/api/apps/api/src/modules/published-project/dto/create-published-project.dto.ts +++ b/api/apps/api/src/modules/published-project/dto/create-published-project.dto.ts @@ -17,7 +17,9 @@ export interface CreatePublishedProjectDto { id: string; name?: string; description?: string; + location?: string; creators?: Creator[]; resources?: Resource[]; company?: Company; + pngData?: string; } diff --git a/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts b/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts index f1535b34d5..9c671edd48 100644 --- a/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts +++ b/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts @@ -1,5 +1,7 @@ +import { WebshotConfig } from '@marxan/webshot'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { IsOptional, IsString, ValidateNested } from 'class-validator'; import { Company, Creator, Resource } from './create-published-project.dto'; export class PublishProjectDto { @@ -13,6 +15,10 @@ export class PublishProjectDto { @ApiPropertyOptional() description?: string; + @IsOptional() + @ApiPropertyOptional() + location?: string; + @IsOptional() @ApiPropertyOptional() creators?: Creator[]; @@ -28,4 +34,9 @@ export class PublishProjectDto { @IsOptional() @ApiPropertyOptional() scenarioId?: string; + + @ApiProperty() + @ValidateNested({ each: true }) + @Type(() => WebshotConfig) + config?: WebshotConfig; } diff --git a/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts b/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts index cad117c278..2e122e7c3d 100644 --- a/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts +++ b/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts @@ -5,6 +5,7 @@ import { Creator, Resource, } from '../dto/create-published-project.dto'; +import { string } from 'fp-ts'; @Entity('published_projects') export class PublishedProject { @@ -17,6 +18,9 @@ export class PublishedProject { @Column('character varying') description?: string; + @Column('character varying') + location?: string; + @Column('boolean', { name: 'under_moderation', default: false }) underModeration?: boolean; @@ -29,6 +33,9 @@ export class PublishedProject { @Column('jsonb') creators?: Creator[]; + @Column('character varying', { name: 'png_data' }) + pngData?: string; + @OneToOne(() => Project) @JoinColumn({ name: 'id' }) originalProject?: Project; diff --git a/api/apps/api/src/modules/published-project/published-project.module.ts b/api/apps/api/src/modules/published-project/published-project.module.ts index d48bee8511..17d073c841 100644 --- a/api/apps/api/src/modules/published-project/published-project.module.ts +++ b/api/apps/api/src/modules/published-project/published-project.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { PublishedProjectService } from './published-project.service'; import { ProjectsModule } from '@marxan-api/modules/projects/projects.module'; import { PublishedProjectCrudService } from '@marxan-api/modules/published-project/published-project-crud.service'; @@ -9,6 +9,7 @@ import { PublishProjectController } from '@marxan-api/modules/published-project/ import { PublishedProjectSerializer } from '@marxan-api/modules/published-project/published-project.serializer'; import { AccessControlModule } from '@marxan-api/modules/access-control'; import { UsersModule } from '@marxan-api/modules/users/users.module'; +import { WebshotModule } from '@marxan/webshot'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { UsersModule } from '@marxan-api/modules/users/users.module'; ProjectsModule, TypeOrmModule.forFeature([PublishedProject]), UsersModule, + forwardRef(() => WebshotModule), ], controllers: [PublishProjectController, PublishedProjectReadController], providers: [ diff --git a/api/apps/api/src/modules/published-project/published-project.service.ts b/api/apps/api/src/modules/published-project/published-project.service.ts index 774a3263d6..b376f2f0e0 100644 --- a/api/apps/api/src/modules/published-project/published-project.service.ts +++ b/api/apps/api/src/modules/published-project/published-project.service.ts @@ -10,6 +10,10 @@ import { PublishedProject } from '@marxan-api/modules/published-project/entities import { ProjectAccessControl } from '@marxan-api/modules/access-control'; import { UsersService } from '@marxan-api/modules/users/users.service'; import { PublishProjectDto } from './dto/publish-project.dto'; +import { WebshotService } from '@marxan/webshot'; +import { AppConfig } from '@marxan-api/utils/config.utils'; +import { assertDefined } from '@marxan/utils'; +import { isLeft, isRight } from 'fp-ts/lib/These'; export const notFound = Symbol(`project not found`); export const accessDenied = Symbol(`not allowed`); @@ -33,6 +37,7 @@ export class PublishedProjectService { @InjectRepository(PublishedProject) private publicProjectsRepo: Repository, private crudService: PublishedProjectCrudService, + private webshotService: WebshotService, private readonly acl: ProjectAccessControl, private readonly usersService: UsersService, ) {} @@ -59,13 +64,36 @@ export class PublishedProjectService { return left(alreadyPublished); } - // @debt scenarioId will be used in the png_map_data generation. - const { scenarioId, ...projectWithoutScenario } = projectToPublish; + const webshotUrl = AppConfig.get('webshot.url') as string; - await this.crudService.create({ + const { scenarioId, config, ...projectWithoutScenario } = projectToPublish; + assertDefined(scenarioId); + assertDefined(config); + const pngData = await this.webshotService.getPublishedProjectsImage( + scenarioId, id, - ...projectWithoutScenario, - }); + { + ...config, + screenshotOptions: { + clip: { x: 0, y: 0, width: 500, height: 500 }, + }, + }, + webshotUrl, + ); + + if (isLeft(pngData)) { + console.info( + `Map screenshot for public project ${id} could not be generated`, + ); + } + + if (isRight(pngData)) { + await this.crudService.create({ + id, + ...projectWithoutScenario, + pngData: pngData.right, + }); + } return right(true); } diff --git a/api/libs/webshot/src/webshot.service.ts b/api/libs/webshot/src/webshot.service.ts index 8e1a7d5b7d..a8c89db2a5 100644 --- a/api/libs/webshot/src/webshot.service.ts +++ b/api/libs/webshot/src/webshot.service.ts @@ -67,4 +67,31 @@ export class WebshotService { return left(unknownPngWebshotError); } } + + async getPublishedProjectsImage( + scenarioId: string, + projectId: string, + config: WebshotPngConfig, + webshotUrl: string, + ): Promise> { + try { + const pngBuffer = await this.httpService + .post( + `${webshotUrl}/projects/${projectId}/scenarios/${scenarioId}/published-project/frequency`, + config, + { responseType: 'arraybuffer' }, + ) + .toPromise() + .then((response) => response.data) + .catch((error) => { + throw new Error(error); + }); + + const pngBase64String = Buffer.from(pngBuffer).toString('base64'); + + return right(pngBase64String); + } catch (error) { + return left(unknownPngWebshotError); + } + } } From 9235d0f75110f53168312d2900d4f0830272bc7f Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Fri, 22 Apr 2022 12:07:19 +0200 Subject: [PATCH 2/7] feat: adds webshot service api and method for public projects images --- api/libs/webshot/src/webshot.service.ts | 2 +- .../published-projects-maps.ts | 99 +++++++++++++++++++ webshot/src/main.ts | 11 +++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 webshot/src/domain/published-project-maps/published-projects-maps.ts diff --git a/api/libs/webshot/src/webshot.service.ts b/api/libs/webshot/src/webshot.service.ts index a8c89db2a5..4d6a428ae8 100644 --- a/api/libs/webshot/src/webshot.service.ts +++ b/api/libs/webshot/src/webshot.service.ts @@ -77,7 +77,7 @@ export class WebshotService { try { const pngBuffer = await this.httpService .post( - `${webshotUrl}/projects/${projectId}/scenarios/${scenarioId}/published-project/frequency`, + `${webshotUrl}/projects/${projectId}/scenarios/${scenarioId}/published-projects/frequency`, config, { responseType: 'arraybuffer' }, ) diff --git a/webshot/src/domain/published-project-maps/published-projects-maps.ts b/webshot/src/domain/published-project-maps/published-projects-maps.ts new file mode 100644 index 0000000000..8b3e9575a3 --- /dev/null +++ b/webshot/src/domain/published-project-maps/published-projects-maps.ts @@ -0,0 +1,99 @@ +import { Request, Response } from "express"; +import puppeteer, { ScreenshotOptions } from "puppeteer"; +import { passthroughConsole } from "../../utils/passthrough-console.utils"; +import { waitForReportReady } from "../../utils/wait-function"; + +const appRouteTemplate = + "/reports/:projectId/:scenarioId/frequency"; + +export const generatePngImageFromPublishedProjectData = async ( + req: Request, + res: Response +) => { + const { + body: { baseUrl, cookie, screenshotOptions }, + }: { + body: { + baseUrl: string; + cookie: string; + screenshotOptions: ScreenshotOptions; + }; + } = req; + + const { + params: { projectId, scenarioId }, + } = req; + + if (!(projectId || scenarioId )) { + res.status(400).json({ + error: `Invalid request: projectId (${projectId}) or scenarioId (${scenarioId}) were not provided.`, + }); + return; + } + + if (!baseUrl) { + res.status(400).json({ error: "No baseUrl was provided." }); + return; + } + + try { + new URL(baseUrl); + } catch (error) { + res + .status(400) + .json({ error: `The provided baseUrl (${baseUrl} is not a valid URL.` }); + return; + } + + const pageUrl = `${baseUrl}${appRouteTemplate + .replace(":projectId", projectId) + .replace(":scenarioId", scenarioId)}`; + + const browser = await puppeteer.launch({ + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + '--disable-web-security', + "--disable-features=IsolateOrigins", + "--disable-site-isolation-trials", + ], + }); + const page = await browser.newPage(); + // Pass through browser console to our own service's console + page.on("console", passthroughConsole); + + /** + * The webshot service authenticates to the upstream frontend instance by + * passing through the cookie that it receives from the API. In practice, all + * that is needed is the `__Secure-next-auth.session-token` cookie (or + * `next-auth.session-token` in development environments where the frontend + * may not be running behind an HTTPS reverse proxy). + * + * @todo remove Bypass-Tunnel-Reminder once done with all development and + * checks via LocalTunnel; the following line will do instead. + * + * if (cookie) await page.setExtraHTTPHeaders({ cookie }); + */ + if (cookie) { + await page.setExtraHTTPHeaders({ + cookie, + "Bypass-Tunnel-Reminder": "true", + }); + } else { + await page.setExtraHTTPHeaders({ "Bypass-Tunnel-Reminder": "true" }); + } + + console.info(`Rendering ${pageUrl} as PNG`); + await page.goto(pageUrl); + await page.waitForFunction(waitForReportReady, { timeout: 30e3 }); + + const pageAsPng = await Promise.race([ + page.screenshot({ ...screenshotOptions }), + new Promise((resolve, reject) => setTimeout(reject, 30e3)), + ]); + await page.close(); + await browser.close(); + + res.type("image/png"); + res.end(pageAsPng); +}; diff --git a/webshot/src/main.ts b/webshot/src/main.ts index 3e157a2d1b..8dfbf6e790 100644 --- a/webshot/src/main.ts +++ b/webshot/src/main.ts @@ -10,6 +10,7 @@ import config from "config"; import helmet from "helmet"; import { generateSummaryReportForScenario } from "./domain/solutions-report/solutions-report"; import { generatePngImageFromBlmData } from "./domain/blm-previews/png-image"; +import { generatePngImageFromPublishedProjectData } from "./domain/published-project-maps/published-projects-maps"; const app: Application = express(); const daemonListenPort = config.get("port"); @@ -43,6 +44,16 @@ app.post( } ); +app.post( + "/projects/:projectId/scenarios/:scenarioId/published-projects/frequency", + async (req: Request, res: Response, next: NextFunction) => { + await generatePngImageFromPublishedProjectData(req, res).catch((error) => { + console.error(error); + next(error); + }); + } +); + app.get("/api/ping", async (req: Request, res: Response) => { res.status(200).json({ ping: "pong" }); }); From 04219b1494c7a19799f1be02de55cf4f4d11e285 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Fri, 22 Apr 2022 12:07:37 +0200 Subject: [PATCH 3/7] minor: removes redundant test data fixtures --- api/apps/api/test/fixtures/test-data.sql | 9 --------- 1 file changed, 9 deletions(-) diff --git a/api/apps/api/test/fixtures/test-data.sql b/api/apps/api/test/fixtures/test-data.sql index 9f148adc31..b15dbbb1b7 100644 --- a/api/apps/api/test/fixtures/test-data.sql +++ b/api/apps/api/test/fixtures/test-data.sql @@ -41,15 +41,6 @@ VALUES ('Example scenario 1 Project 2 Org 2', (select id from projects where name = 'Example Project 2 Org 2'), 'marxan', 30, 100, 1, (SELECT id FROM users WHERE email = 'aa@example.com') ), ('Example scenario 2 Project 2 Org 2', (select id from projects where name = 'Example Project 2 Org 2'), 'marxan', 50, 100, 1, (SELECT id FROM users WHERE email = 'aa@example.com') ); -INSERT INTO users_scenarios -(user_id, scenario_id, role_id) -VALUES -((SELECT id FROM users WHERE lower(email) = 'aa@example.com'), (SELECT id FROM scenarios WHERE name = 'Example scenario 1 Project 2 Org 2'), 'scenario_owner'), -((SELECT id FROM users WHERE lower(email) = 'bb@example.com'), (SELECT id FROM scenarios WHERE name = 'Example scenario 1 Project 2 Org 2'), 'scenario_contributor'), -((SELECT id FROM users WHERE lower(email) = 'cc@example.com'), (SELECT id FROM scenarios WHERE name = 'Example scenario 1 Project 2 Org 2'), 'scenario_viewer'), -((SELECT id FROM users WHERE lower(email) = 'aa@example.com'), (SELECT id FROM scenarios WHERE name = 'Example scenario 2 Project 2 Org 2'), 'scenario_owner'), -((SELECT id FROM users WHERE lower(email) = 'bb@example.com'), (SELECT id FROM scenarios WHERE name = 'Example scenario 2 Project 2 Org 2'), 'scenario_contributor'), -((SELECT id FROM users WHERE lower(email) = 'cc@example.com'), (SELECT id FROM scenarios WHERE name = 'Example scenario 2 Project 2 Org 2'), 'scenario_viewer'); INSERT INTO platform_admins (user_id) From 419b507d196def47447e53fce75c0fd35acf7708 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Fri, 22 Apr 2022 17:00:56 +0200 Subject: [PATCH 4/7] feat: adapts tests to pngData introduction --- .../published-project.service.ts | 19 +++++++++------ .../api/test/project/projects.fixtures.ts | 23 +++++++++++++++++- .../test/project/public-projects.e2e-spec.ts | 24 ++++++++++++------- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/api/apps/api/src/modules/published-project/published-project.service.ts b/api/apps/api/src/modules/published-project/published-project.service.ts index b376f2f0e0..c62c8608eb 100644 --- a/api/apps/api/src/modules/published-project/published-project.service.ts +++ b/api/apps/api/src/modules/published-project/published-project.service.ts @@ -64,6 +64,12 @@ export class PublishedProjectService { return left(alreadyPublished); } + // @debt If we end up moving the scenario map thumbnail generation + // to the end of the Marxan run process, this part here regarding + // the webshot should be removed and/or adapted to it. It looks + // like it does not belong here at all anyways, but right now there + // is not a better place to deal with this. + const webshotUrl = AppConfig.get('webshot.url') as string; const { scenarioId, config, ...projectWithoutScenario } = projectToPublish; @@ -87,13 +93,12 @@ export class PublishedProjectService { ); } - if (isRight(pngData)) { - await this.crudService.create({ - id, - ...projectWithoutScenario, - pngData: pngData.right, - }); - } + await this.crudService.create({ + id, + ...projectWithoutScenario, + pngData: isRight(pngData) ? pngData.right : '', + }); + return right(true); } diff --git a/api/apps/api/test/project/projects.fixtures.ts b/api/apps/api/test/project/projects.fixtures.ts index 7da7c57ad8..b1eeaaf189 100644 --- a/api/apps/api/test/project/projects.fixtures.ts +++ b/api/apps/api/test/project/projects.fixtures.ts @@ -23,6 +23,8 @@ import { GivenProjectExists } from '../steps/given-project'; import { GivenUserIsLoggedIn } from '../steps/given-user-is-logged-in'; import { bootstrapApplication } from '../utils/api-application'; import { EventBusTestUtils } from '../utils/event-bus.test.utils'; +import { ScenariosTestUtils } from '../utils/scenarios.test.utils'; +import { ScenarioType } from '@marxan-api/modules/scenarios/scenario.api.entity'; export const getFixtures = async () => { const app = await bootstrapApplication([CqrsModule], [EventBusTestUtils]); @@ -83,6 +85,19 @@ export const getFixtures = async () => { cleanups.push(cleanup); return projectId; }, + GivenScenarioWasCreated: async (projectId: string) => { + const result = await ScenariosTestUtils.createScenario( + app, + randomUserToken, + { + name: `Test scenario`, + type: ScenarioType.marxan, + projectId, + }, + ); + + return result.data.id; + }, GivenPublicProjectWasCreated: async () => { const { projectId, cleanup } = await GivenProjectExists( app, @@ -179,6 +194,8 @@ export const getFixtures = async () => { company: null, resources: null, creators: null, + location: null, + pngData: null, }, id: publicProjectId, type: 'published_projects', @@ -207,6 +224,7 @@ export const getFixtures = async () => { }, ], resources: [{ title: expect.any(String), url: expect.any(String) }], + location: expect.any(String), }, id: publicProjectId, type: 'published_projects', @@ -253,15 +271,18 @@ export const getFixtures = async () => { await request(app.getHttpServer()) .get(`/api/v1/projects/${projectId}`) .set('Authorization', `Bearer ${notIncludedUserToken}`), - WhenPublishingAProject: async (projectId: string) => + WhenPublishingAProject: async (projectId: string, scenarioId: string) => await request(app.getHttpServer()) .post(`/api/v1/projects/${projectId}/publish`) .send({ name: 'example project', description: 'fake description', + location: 'fake location', company: { name: 'logo', logoDataUrl: blmImageMock }, creators: [{ displayName: 'fake name', avatarDataUrl: blmImageMock }], resources: [{ title: 'fake url', url: 'http://www.example.com' }], + config: { baseUrl: 'example/png', cookie: 'randomCookie' }, + scenarioId, }) .set('Authorization', `Bearer ${randomUserToken}`), WhenUnpublishingAProjectAsProjectOwner: async (projectId: string) => diff --git a/api/apps/api/test/project/public-projects.e2e-spec.ts b/api/apps/api/test/project/public-projects.e2e-spec.ts index 51e054d25f..25c18d9dd0 100644 --- a/api/apps/api/test/project/public-projects.e2e-spec.ts +++ b/api/apps/api/test/project/public-projects.e2e-spec.ts @@ -25,7 +25,8 @@ test(`getting public projects`, async () => { test(`publishing a project`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenGettingPublicProjects(); fixtures.ThenPublicProjectIsAvailable(projectId, response); @@ -33,7 +34,8 @@ test(`publishing a project`, async () => { test(`when placing a public project under moderation as a platform admin`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin( projectId, @@ -61,7 +63,8 @@ test(`when placing a public project under moderation as a platform admin`, async test(`when clearing under moderation status from a public project as a platform admin`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin( projectId, @@ -82,7 +85,8 @@ test(`when clearing under moderation status from a public project as a platform test(`when placing a public project under moderation as not a platform admin`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenPlacingAPublicProjectUnderModerationNotAsAdmin( projectId, @@ -96,7 +100,8 @@ test(`when placing a public project under moderation as not a platform admin`, a test(`when clearing under moderation status from a public project not as platform admin`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin( projectId, @@ -119,7 +124,8 @@ test(`when clearing under moderation status from a public project not as platfor test(`when unpublishing a public project as a project owner`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenUnpublishingAProjectAsProjectOwner(projectId); @@ -130,7 +136,8 @@ test(`when unpublishing a public project as a project owner`, async () => { test(`when unpublishing a public project that is under moderation as a project owner`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin( projectId, @@ -145,7 +152,8 @@ test(`when unpublishing a public project that is under moderation as a project o test(`when unpublishing a public project that is under moderation as a platform admin`, async () => { const projectId = await fixtures.GivenPrivateProjectWasCreated(); - let response = await fixtures.WhenPublishingAProject(projectId); + const scenarioId = await fixtures.GivenScenarioWasCreated(projectId); + let response = await fixtures.WhenPublishingAProject(projectId, scenarioId); fixtures.ThenCreatedIsReturned(response); response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin( projectId, From 3762860eff8ace45fec03be6290ebec4f0f47766 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Mon, 25 Apr 2022 09:30:14 +0200 Subject: [PATCH 5/7] feat: adds missing properties in public project data --- .../modules/published-project/published-project-crud.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/apps/api/src/modules/published-project/published-project-crud.service.ts b/api/apps/api/src/modules/published-project/published-project-crud.service.ts index c999269acb..a2381fa1d8 100644 --- a/api/apps/api/src/modules/published-project/published-project-crud.service.ts +++ b/api/apps/api/src/modules/published-project/published-project-crud.service.ts @@ -41,11 +41,13 @@ export class PublishedProjectCrudService extends AppBaseService< attributes: [ 'name', 'description', + 'location', 'creators', 'company', 'resources', 'underModeration', 'originalProject', + 'pngData', ], keyForAttribute: 'camelCase', originalProject: { From 4fa4bd31394f932a6b9b546e6a02ea4744b60be7 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Mon, 25 Apr 2022 13:33:38 +0200 Subject: [PATCH 6/7] feat: missing attributes in tests --- api/apps/api/test/project/projects.fixtures.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/apps/api/test/project/projects.fixtures.ts b/api/apps/api/test/project/projects.fixtures.ts index b1eeaaf189..d82e40bff6 100644 --- a/api/apps/api/test/project/projects.fixtures.ts +++ b/api/apps/api/test/project/projects.fixtures.ts @@ -225,6 +225,7 @@ export const getFixtures = async () => { ], resources: [{ title: expect.any(String), url: expect.any(String) }], location: expect.any(String), + pngData: expect.any(String), }, id: publicProjectId, type: 'published_projects', @@ -242,6 +243,7 @@ export const getFixtures = async () => { name: expect.any(String), underModeration: true, description: expect.any(String), + location: expect.any(String), company: { name: expect.any(String), logoDataUrl: expect.any(String), @@ -253,6 +255,7 @@ export const getFixtures = async () => { }, ], resources: [{ title: expect.any(String), url: expect.any(String) }], + pngData: expect.any(String), }, id: publicProjectId, type: 'published_projects', From 12ed10ac9597454a9145f2f864ad25f6da0aaeb0 Mon Sep 17 00:00:00 2001 From: Ruben Vallejo Date: Tue, 26 Apr 2022 11:58:36 +0200 Subject: [PATCH 7/7] minor: fixes of unused imports, wrong imports, renaming dto stuff --- .../published-project/dto/publish-project.dto.ts | 2 +- .../entities/published-project.api.entity.ts | 1 - .../published-project/published-project.service.ts | 14 ++++++++++---- api/apps/api/test/project/projects.fixtures.ts | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts b/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts index 9c671edd48..e3dad99307 100644 --- a/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts +++ b/api/apps/api/src/modules/published-project/dto/publish-project.dto.ts @@ -33,7 +33,7 @@ export class PublishProjectDto { @IsOptional() @ApiPropertyOptional() - scenarioId?: string; + featuredScenarioId?: string; @ApiProperty() @ValidateNested({ each: true }) diff --git a/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts b/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts index 2e122e7c3d..298ecadb11 100644 --- a/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts +++ b/api/apps/api/src/modules/published-project/entities/published-project.api.entity.ts @@ -5,7 +5,6 @@ import { Creator, Resource, } from '../dto/create-published-project.dto'; -import { string } from 'fp-ts'; @Entity('published_projects') export class PublishedProject { diff --git a/api/apps/api/src/modules/published-project/published-project.service.ts b/api/apps/api/src/modules/published-project/published-project.service.ts index c62c8608eb..cbcc0e19c0 100644 --- a/api/apps/api/src/modules/published-project/published-project.service.ts +++ b/api/apps/api/src/modules/published-project/published-project.service.ts @@ -13,7 +13,7 @@ import { PublishProjectDto } from './dto/publish-project.dto'; import { WebshotService } from '@marxan/webshot'; import { AppConfig } from '@marxan-api/utils/config.utils'; import { assertDefined } from '@marxan/utils'; -import { isLeft, isRight } from 'fp-ts/lib/These'; +import { isLeft, isRight } from 'fp-ts/lib/Either'; export const notFound = Symbol(`project not found`); export const accessDenied = Symbol(`not allowed`); @@ -72,11 +72,17 @@ export class PublishedProjectService { const webshotUrl = AppConfig.get('webshot.url') as string; - const { scenarioId, config, ...projectWithoutScenario } = projectToPublish; - assertDefined(scenarioId); + const { + featuredScenarioId, + config, + ...projectWithoutScenario + } = projectToPublish; + + assertDefined(featuredScenarioId); assertDefined(config); + const pngData = await this.webshotService.getPublishedProjectsImage( - scenarioId, + featuredScenarioId, id, { ...config, diff --git a/api/apps/api/test/project/projects.fixtures.ts b/api/apps/api/test/project/projects.fixtures.ts index d82e40bff6..41a24b41f4 100644 --- a/api/apps/api/test/project/projects.fixtures.ts +++ b/api/apps/api/test/project/projects.fixtures.ts @@ -285,7 +285,7 @@ export const getFixtures = async () => { creators: [{ displayName: 'fake name', avatarDataUrl: blmImageMock }], resources: [{ title: 'fake url', url: 'http://www.example.com' }], config: { baseUrl: 'example/png', cookie: 'randomCookie' }, - scenarioId, + featuredScenarioId: scenarioId, }) .set('Authorization', `Bearer ${randomUserToken}`), WhenUnpublishingAProjectAsProjectOwner: async (projectId: string) =>