Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MARXAN-1274-publicMap-screenshot #1007

Merged
merged 7 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddLocationsPngDataPublicProject1650619314086
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE published_projects
ADD COLUMN location varchar,
ADD COLUMN png_data text;
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE published_projects
DROP COLUMN location,
DROP COLUMN png_data;
`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export interface CreatePublishedProjectDto {
id: string;
name?: string;
description?: string;
location?: string;
creators?: Creator[];
resources?: Resource[];
company?: Company;
pngData?: string;
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,6 +15,10 @@ export class PublishProjectDto {
@ApiPropertyOptional()
description?: string;

@IsOptional()
@ApiPropertyOptional()
location?: string;

@IsOptional()
@ApiPropertyOptional()
creators?: Creator[];
Expand All @@ -27,5 +33,10 @@ export class PublishProjectDto {

@IsOptional()
@ApiPropertyOptional()
scenarioId?: string;
featuredScenarioId?: string;

@ApiProperty()
@ValidateNested({ each: true })
@Type(() => WebshotConfig)
config?: WebshotConfig;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export class PublishedProject {
@Column('character varying')
description?: string;

@Column('character varying')
location?: string;

@Column('boolean', { name: 'under_moderation', default: false })
underModeration?: boolean;

Expand All @@ -29,6 +32,9 @@ export class PublishedProject {
@Column('jsonb')
creators?: Creator[];

@Column('character varying', { name: 'png_data' })
pngData?: string;

@OneToOne(() => Project)
@JoinColumn({ name: 'id' })
originalProject?: Project;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ export class PublishedProjectCrudService extends AppBaseService<
attributes: [
'name',
'description',
'location',
'creators',
'company',
'resources',
'underModeration',
'originalProject',
'pngData',
],
keyForAttribute: 'camelCase',
originalProject: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,13 +9,15 @@ 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: [
AccessControlModule,
ProjectsModule,
TypeOrmModule.forFeature([PublishedProject]),
UsersModule,
forwardRef(() => WebshotModule),
],
controllers: [PublishProjectController, PublishedProjectReadController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/Either';

export const notFound = Symbol(`project not found`);
export const accessDenied = Symbol(`not allowed`);
Expand All @@ -33,6 +37,7 @@ export class PublishedProjectService {
@InjectRepository(PublishedProject)
private publicProjectsRepo: Repository<PublishedProject>,
private crudService: PublishedProjectCrudService,
private webshotService: WebshotService,
private readonly acl: ProjectAccessControl,
private readonly usersService: UsersService,
) {}
Expand All @@ -59,13 +64,47 @@ export class PublishedProjectService {
return left(alreadyPublished);
}

// @debt scenarioId will be used in the png_map_data generation.
const { scenarioId, ...projectWithoutScenario } = projectToPublish;
// @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 {
featuredScenarioId,
config,
...projectWithoutScenario
} = projectToPublish;

assertDefined(featuredScenarioId);
assertDefined(config);

const pngData = await this.webshotService.getPublishedProjectsImage(
featuredScenarioId,
id,
{
...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`,
);
}

await this.crudService.create({
id,
...projectWithoutScenario,
pngData: isRight(pngData) ? pngData.right : '',
});

return right(true);
}

Expand Down
9 changes: 0 additions & 9 deletions api/apps/api/test/fixtures/test-data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '[email protected]') ),
('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 = '[email protected]') );

INSERT INTO users_scenarios
(user_id, scenario_id, role_id)
VALUES
((SELECT id FROM users WHERE lower(email) = '[email protected]'), (SELECT id FROM scenarios WHERE name = 'Example scenario 1 Project 2 Org 2'), 'scenario_owner'),
((SELECT id FROM users WHERE lower(email) = '[email protected]'), (SELECT id FROM scenarios WHERE name = 'Example scenario 1 Project 2 Org 2'), 'scenario_contributor'),
((SELECT id FROM users WHERE lower(email) = '[email protected]'), (SELECT id FROM scenarios WHERE name = 'Example scenario 1 Project 2 Org 2'), 'scenario_viewer'),
((SELECT id FROM users WHERE lower(email) = '[email protected]'), (SELECT id FROM scenarios WHERE name = 'Example scenario 2 Project 2 Org 2'), 'scenario_owner'),
((SELECT id FROM users WHERE lower(email) = '[email protected]'), (SELECT id FROM scenarios WHERE name = 'Example scenario 2 Project 2 Org 2'), 'scenario_contributor'),
((SELECT id FROM users WHERE lower(email) = '[email protected]'), (SELECT id FROM scenarios WHERE name = 'Example scenario 2 Project 2 Org 2'), 'scenario_viewer');
hotzevzl marked this conversation as resolved.
Show resolved Hide resolved

INSERT INTO platform_admins
(user_id)
Expand Down
26 changes: 25 additions & 1 deletion api/apps/api/test/project/projects.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -179,6 +194,8 @@ export const getFixtures = async () => {
company: null,
resources: null,
creators: null,
location: null,
pngData: null,
},
id: publicProjectId,
type: 'published_projects',
Expand Down Expand Up @@ -207,6 +224,8 @@ 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',
Expand All @@ -224,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),
Expand All @@ -235,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',
Expand All @@ -253,15 +274,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' },
featuredScenarioId: scenarioId,
})
.set('Authorization', `Bearer ${randomUserToken}`),
WhenUnpublishingAProjectAsProjectOwner: async (projectId: string) =>
Expand Down
24 changes: 16 additions & 8 deletions api/apps/api/test/project/public-projects.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ 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);
});

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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions api/libs/webshot/src/webshot.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,31 @@ export class WebshotService {
return left(unknownPngWebshotError);
}
}

async getPublishedProjectsImage(
scenarioId: string,
projectId: string,
config: WebshotPngConfig,
webshotUrl: string,
): Promise<Either<typeof unknownPngWebshotError, string>> {
try {
const pngBuffer = await this.httpService
.post(
`${webshotUrl}/projects/${projectId}/scenarios/${scenarioId}/published-projects/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);
}
}
}
Loading