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-1611-solutions-are-locked #1117

Merged
merged 7 commits into from
May 27, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddSolutionsAreLockedColumnScenarios1653560528861
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE scenarios
ADD COLUMN solutions_are_locked boolean not null default false;
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE scenarios
DROP COLUMN solutions_are_locked;
`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export type StartLegacyProjectImportResponse = Either<
>;

export class StartLegacyProjectImport extends Command<StartLegacyProjectImportResponse> {
constructor(public readonly name: string, public readonly ownerId: UserId) {
constructor(
public readonly name: string,
public readonly ownerId: UserId,
public readonly solutionsAreLocked: boolean,
) {
super();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const getFixtures = async () => {
},
WhenStartingLegacyProjectImport: () => {
return sut.execute(
new StartLegacyProjectImport('random project name', ownerId),
new StartLegacyProjectImport('random project name', ownerId, false),
);
},
ThenAStartingLegacyProjectIsNotCreated: async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class StartLegacyProjectImportHandler
private readonly legacyProjectImportRepository: LegacyProjectImportRepository,
) {}

private async createShells(name: string) {
private async createShells(name: string, solutionsAreLocked: boolean) {
try {
const [randomOrganization] = await this.organizationRepo.find({
take: 1,
Expand All @@ -49,6 +49,7 @@ export class StartLegacyProjectImportHandler
const scenario = await this.scenarioRepo.save({
name,
projectId: project.id,
solutionsAreLocked,
});

return right({
Expand All @@ -63,8 +64,9 @@ export class StartLegacyProjectImportHandler
async execute({
name,
ownerId,
solutionsAreLocked,
}: StartLegacyProjectImport): Promise<StartLegacyProjectImportResponse> {
const shellsOrError = await this.createShells(name);
const shellsOrError = await this.createShells(name, solutionsAreLocked);

if (isLeft(shellsOrError)) return shellsOrError;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
LegacyProjectImportPiece,
} from '@marxan/legacy-project-import';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsString } from 'class-validator';
import { IsBoolean, IsEnum, IsString } from 'class-validator';
import { LegacyProjectImportComponentReport } from '../../legacy-project-import/application/get-legacy-project-import-errors.query';
import { LegacyProjectImportComponentStatuses } from '../../legacy-project-import/domain/legacy-project-import/legacy-project-import-component-status';

Expand All @@ -14,6 +14,13 @@ export class StartLegacyProjectImportBodyDto {
})
@IsString()
projectName!: string;

@ApiProperty({
description:
'Flag to let the user lock solutions while legacy import is processed',
})
@IsBoolean()
solutionsAreLocked!: boolean;
aciddaute marked this conversation as resolved.
Show resolved Hide resolved
}

export class StartLegacyProjectImportResponseDto {
Expand Down
1 change: 1 addition & 0 deletions api/apps/api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export class ProjectsController {
const result = await this.projectsService.startLegacyProjectImport(
dto.projectName,
req.user.id,
dto.solutionsAreLocked,
);

if (isLeft(result)) {
Expand Down
7 changes: 6 additions & 1 deletion api/apps/api/src/modules/projects/projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ export class ProjectsService {
async startLegacyProjectImport(
projectName: string,
userId: string,
solutionsAreLocked: boolean,
): Promise<
Either<
typeof forbiddenError | StartLegacyProjectImportError,
Expand All @@ -473,7 +474,11 @@ export class ProjectsService {
}

return this.commandBus.execute(
new StartLegacyProjectImport(projectName, new UserId(userId)),
new StartLegacyProjectImport(
projectName,
new UserId(userId),
solutionsAreLocked,
),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ async function getFixtures() {
type: ScenarioType.marxanWithZones,
users: [],
ranAtLeastOnce: false,
solutionsAreLocked: false,
};
},
withInputParameters() {
Expand Down
4 changes: 3 additions & 1 deletion api/apps/api/src/modules/scenarios/scenario.api.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { User } from '../users/user.api.entity';
import { IsArray, IsOptional } from 'class-validator';
import { TimeUserEntityMetadata } from '../../types/time-user-entity-metadata';
import { BaseServiceResource } from '../../types/resource.interface';
import { GeoFeatureSetSpecification } from '../geo-features/dto/geo-feature-set-specification.dto';
import { JsonApiAsyncJobMeta } from '@marxan-api/dto/async-job.dto';
import { ScenarioBlm } from '@marxan-api/modules/blm/values/repositories/scenario-blm/scenario-blm.api.entity';

Expand Down Expand Up @@ -78,6 +77,9 @@ export class Scenario extends TimeUserEntityMetadata {
@OneToOne(() => ScenarioBlm)
scenarioBlm!: ScenarioBlm;

@Column('boolean', { name: 'solutions_are_locked', default: false })
solutionsAreLocked!: boolean;

/**
* The project to which this scenario belongs.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class ScenariosCrudService extends AppBaseService<
'createdByUser',
'lastModifiedAt',
'ranAtLeastOnce',
'solutionsAreLocked',
],
keyForAttribute: 'camelCase',
project: {
Expand Down
12 changes: 9 additions & 3 deletions api/apps/api/src/modules/scenarios/scenarios.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
HttpService,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException,
} from '@nestjs/common';
import { FetchSpecification } from 'nestjs-base-service';
Expand Down Expand Up @@ -112,7 +111,6 @@ import {
import { InjectEntityManager } from '@nestjs/typeorm';
import { apiConnections } from '@marxan-api/ormconfig';
import { EntityManager } from 'typeorm';
import { blmImageMock } from './__mock__/blm-image-mock';
import { UserId } from '@marxan/domain-ids';

/** @debt move to own module */
Expand All @@ -127,6 +125,9 @@ export type ProjectNotReady = typeof projectNotReady;
export const bestSolutionNotFound = Symbol('best solution not found');

export const projectDoesntExist = Symbol(`project doesn't exist`);
export const lockedSolutions = Symbol(
`solutions from this scenario are locked`,
);
export type ProjectDoesntExist = typeof projectDoesntExist;

export type SubmitProtectedAreaError =
Expand Down Expand Up @@ -540,7 +541,8 @@ export class ScenariosService {
| typeof forbiddenError
| typeof noLockInPlace
| typeof lockedByAnotherUser
| typeof scenarioNotFound,
| typeof scenarioNotFound
| typeof lockedSolutions,
void
>
> {
Expand All @@ -550,6 +552,10 @@ export class ScenariosService {

if (isLeft(scenario)) return scenario;

if (scenario.right.solutionsAreLocked) {
return left(lockedSolutions);
}

const userCanEditScenario = await this.scenarioAclService.canEditScenarioAndOwnsLock(
userId,
scenarioId,
Expand Down
6 changes: 6 additions & 0 deletions api/apps/api/src/utils/acl.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
bestSolutionNotFound,
projectDoesntExist,
projectNotReady,
lockedSolutions,
} from '@marxan-api/modules/scenarios/scenarios.service';
import { internalError } from '@marxan-api/modules/specification/application/submit-specification.command';
import { notFound as protectedAreaProjectNotFound } from '@marxan/projects';
Expand Down Expand Up @@ -84,6 +85,7 @@ interface ErrorHandlerOptions {
export const mapAclDomainToHttpError = (
errorToCheck:
| GetScenarioFailure
| typeof lockedSolutions
| typeof forbiddenError
| typeof lastOwner
| typeof transactionFailed
Expand Down Expand Up @@ -258,6 +260,10 @@ export const mapAclDomainToHttpError = (
return new ForbiddenException(
`Trying to clone project export with ID ${options?.exportId} which is not a published project`,
);
case lockedSolutions:
return new BadRequestException(
`Scenario ${options?.scenarioId} solutions are locked.`,
);
default:
const _exhaustiveCheck: never = errorToCheck;
return _exhaustiveCheck;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ const getFixtures = async () => {
events.push(event);
});

const startLegacyProjectImport = (name: string = 'Legacy project') =>
const startLegacyProjectImport = (
name: string = 'Legacy project',
solutionsAreLocked: boolean = false,
) =>
request(app.getHttpServer())
.post(`/api/v1/projects/import/legacy`)
.set('Authorization', `Bearer ${token}`)
.send({ projectName: name })
.send({ projectName: name, solutionsAreLocked })
.expect(201);

const runLegacyProjectImport = (projectId: string) =>
Expand Down Expand Up @@ -216,7 +219,7 @@ const getFixtures = async () => {
);
},
WhenInvokingStartEndpoint: async (name: string) => {
const result = await startLegacyProjectImport(name);
const result = await startLegacyProjectImport(name, false);

expect(result.body.projectId).toBeDefined();

Expand Down
3 changes: 3 additions & 0 deletions api/apps/api/test/scenarios/input-file.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ async function getFixtures() {
status: null,
type: 'marxan',
wdpaThreshold: null,
solutionsAreLocked: false,
...metadata,
},
id,
Expand Down Expand Up @@ -674,6 +675,7 @@ async function getFixtures() {
status: null,
type: 'marxan',
wdpaThreshold: null,
solutionsAreLocked: false,
...metadata,
},
id,
Expand Down Expand Up @@ -705,6 +707,7 @@ async function getFixtures() {
status: null,
type: 'marxan',
wdpaThreshold: null,
solutionsAreLocked: false,
...metadata,
},
id,
Expand Down