Skip to content

Commit

Permalink
Merge pull request #913 from Vizzuality/MARXAN-1380-unpublish-endpoin…
Browse files Browse the repository at this point in the history
…t-project-owners

MARXAN-1380-unpublish-public-project
  • Loading branch information
rubvalave authored Mar 23, 2022
2 parents 3ff3f62 + 1e76b53 commit 419cbf6
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
BadRequestException,
Controller,
DefaultValuePipe,
ForbiddenException,
Get,
InternalServerErrorException,
NotFoundException,
Param,
ParseBoolPipe,
Patch,
Post,
Query,
Expand All @@ -20,6 +22,8 @@ import {
internalError,
notFound,
PublishedProjectService,
notPublished,
underModerationError,
} from '../published-project.service';
import { JwtAuthGuard } from '@marxan-api/guards/jwt-auth.guard';
import {
Expand Down Expand Up @@ -91,6 +95,43 @@ export class PublishProjectController {
return;
}

@Post(':id/unpublish')
@ApiNoContentResponse()
@ApiNotFoundResponse()
@ApiForbiddenResponse()
@ApiInternalServerErrorResponse()
async unpublish(
@Param('id') id: string,
@Request() req: RequestWithAuthenticatedUser,
): Promise<void> {
const result = await this.publishedProjectService.unpublish(
id,
req.user.id,
);

if (isLeft(result)) {
switch (result.left) {
case accessDenied:
throw new ForbiddenException();
case notPublished:
throw new BadRequestException('This project is not published yet.');
case notFound:
throw new NotFoundException();
case internalError:
throw new InternalServerErrorException();
case underModerationError:
throw new BadRequestException(
'This project is under moderation and it can not be unpublished.',
);
default:
const _exhaustiveCheck: never = result.left;
throw _exhaustiveCheck;
}
}

return;
}

@Patch(':id/moderation-status/set')
@ApiNoContentResponse()
@ApiNotFoundResponse()
Expand Down Expand Up @@ -135,11 +176,14 @@ export class PublishProjectController {
async clearUnderModeration(
@Param('id') id: string,
@Request() req: RequestWithAuthenticatedUser,
@Query('alsoUnpublish', new DefaultValuePipe(false), ParseBoolPipe)
alsoUnpublish: boolean,
): Promise<void> {
const result = await this.publishedProjectService.changeModerationStatus(
id,
req.user.id,
false,
alsoUnpublish,
);

if (isLeft(result)) {
Expand Down Expand Up @@ -184,7 +228,7 @@ export class PublishProjectController {
description: `A free search over names`,
})
@Get('published-projects/by-admin')
async findOneByAdmin(
async findAllByAdmin(
@ProcessFetchSpecification() fetchSpecification: FetchSpecification,
@Req() req: RequestWithAuthenticatedUser,
@Query('q') namesSearch?: string,
Expand All @@ -205,7 +249,7 @@ export class PublishProjectController {
@ApiOperation({ description: 'Find public project by id for admins.' })
@ApiOkResponse({ type: PublishedProjectResultSingular })
@Get('published-projects/:id/by-admin')
async findAll(
async findOneByAdmin(
@Req() req: RequestWithAuthenticatedUser,
@Param('id') id: string,
): Promise<PublishedProjectResultSingular> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,16 @@ export class PublishedProjectCrudService extends AppBaseService<
fetchSpecification: FetchSpecification,
info?: ProjectsRequest,
): Promise<SelectQueryBuilder<PublishedProject>> {
let showUnderModerationPublishedProjects = false;
const id = info?.authenticatedUser?.id;
if (id && (await this.usersService.isPlatformAdmin(id))) {
showUnderModerationPublishedProjects = true;
}
const userId = info?.authenticatedUser?.id;

query.andWhere('published_project.underModeration = :underModeration', {
underModeration: showUnderModerationPublishedProjects,
});
/*
If we are listing projects for non-authenticated requests or for
authenticated users who are not admin, projects under moderation
will be hiding from the listing.
*/
if (!userId || !(await this.usersService.isPlatformAdmin(userId))) {
query.andWhere('published_project.underModeration is false');
}

return query;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { ProjectsRequest } from '@marxan-api/modules/projects/project-requests-i
import { PublishedProject } from '@marxan-api/modules/published-project/entities/published-project.api.entity';
import { ProjectAccessControl } from '@marxan-api/modules/access-control';
import { UsersService } from '@marxan-api/modules/users/users.service';
import { assertDefined } from '@marxan/utils';

export const notFound = Symbol(`project not found`);
export const accessDenied = Symbol(`not allowed`);
export const underModerationError = Symbol(`this project is under moderation`);
export const sameUnderModerationStatus = Symbol(
`this project is already on that moderation status`,
);
export const alreadyPublished = Symbol(`this project is public`);
export const notPublished = Symbol(`this project is not public yet`);
export const internalError = Symbol(`internal error`);

export type errors =
Expand Down Expand Up @@ -64,10 +65,42 @@ export class PublishedProjectService {
return right(true);
}

async unpublish(
id: string,
requestingUserId: string,
): Promise<
Either<errors | typeof notPublished | typeof underModerationError, true>
> {
const project = await this.projectRepository.findOne(id);

if (!project) {
return left(notFound);
}

const isAdmin = await this.usersService.isPlatformAdmin(requestingUserId);

if (!(await this.acl.canPublishProject(requestingUserId, id)) && !isAdmin) {
return left(accessDenied);
}

const publicProject = await this.publicProjectsRepo.findOne({ id });
if (!publicProject?.id) {
return left(notPublished);
}

if (publicProject.underModeration && !isAdmin) {
return left(underModerationError);
}

await this.publicProjectsRepo.delete({ id });
return right(true);
}

async changeModerationStatus(
id: string,
requestingUserId: string,
status: boolean,
alsoUnpublish?: boolean,
): Promise<Either<errors | typeof sameUnderModerationStatus, true>> {
const existingPublicProject = await this.crudService.getById(
id,
Expand All @@ -89,6 +122,11 @@ export class PublishedProjectService {
await this.crudService.update(id, {
underModeration: !existingPublicProject.underModeration,
});

if (alsoUnpublish) {
await this.unpublish(id, requestingUserId);
}

return right(true);
}

Expand Down
15 changes: 15 additions & 0 deletions api/apps/api/test/project/projects.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export const getFixtures = async () => {
ThenNoContentIsReturned: (response: request.Response) => {
expect(response.status).toEqual(204);
},
ThenBadRequestIsReturned: (response: request.Response) => {
expect(response.status).toEqual(400);
},
ThenNotFoundIsReturned: (response: request.Response) => {
expect(response.status).toEqual(404);
},
Expand Down Expand Up @@ -176,6 +179,10 @@ export const getFixtures = async () => {
await request(app.getHttpServer())
.post(`/api/v1/projects/${projectId}/publish`)
.set('Authorization', `Bearer ${randomUserToken}`),
WhenUnpublishingAProjectAsProjectOwner: async (projectId: string) =>
await request(app.getHttpServer())
.post(`/api/v1/projects/${projectId}/unpublish`)
.set('Authorization', `Bearer ${randomUserToken}`),
WhenPlacingAPublicProjectUnderModerationAsAdmin: async (
projectId: string,
) =>
Expand All @@ -194,6 +201,14 @@ export const getFixtures = async () => {
await request(app.getHttpServer())
.patch(`/api/v1/projects/${projectId}/moderation-status/clear`)
.set('Authorization', `Bearer ${adminUserToken}`),
WhenClearingUnderModerationStatusAndUnpublishingAsAdmin: async (
projectId: string,
) =>
await request(app.getHttpServer())
.patch(
`/api/v1/projects/${projectId}/moderation-status/clear?alsoUnpublish=true`,
)
.set('Authorization', `Bearer ${adminUserToken}`),
WhenClearingUnderModerationStatusFromAPublicProjectNotAsAdmin: async (
projectId: string,
) =>
Expand Down
43 changes: 43 additions & 0 deletions api/apps/api/test/project/public-projects.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,46 @@ test(`when clearing under moderation status from a public project not as platfor
response = await fixtures.WhenGettingPublicProject(projectId);
fixtures.ThenForbiddenIsReturned(response);
});

test(`when unpublishing a public project as a project owner`, async () => {
const projectId = await fixtures.GivenPrivateProjectWasCreated();
let response = await fixtures.WhenPublishingAProject(projectId);
fixtures.ThenCreatedIsReturned(response);

response = await fixtures.WhenUnpublishingAProjectAsProjectOwner(projectId);
fixtures.ThenCreatedIsReturned(response);
response = await fixtures.WhenGettingPublicProjects();
fixtures.ThenNoProjectIsAvailable(response);
});

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);
fixtures.ThenCreatedIsReturned(response);
response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin(
projectId,
);
fixtures.ThenOkIsReturned(response);

response = await fixtures.WhenUnpublishingAProjectAsProjectOwner(projectId);
fixtures.ThenBadRequestIsReturned(response);
response = await fixtures.WhenGettingPublicProjects();
fixtures.ThenNoProjectIsAvailable(response);
});

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);
fixtures.ThenCreatedIsReturned(response);
response = await fixtures.WhenPlacingAPublicProjectUnderModerationAsAdmin(
projectId,
);
fixtures.ThenOkIsReturned(response);

response = await fixtures.WhenClearingUnderModerationStatusAndUnpublishingAsAdmin(
projectId,
);
fixtures.ThenOkIsReturned(response);
response = await fixtures.WhenGettingPublicProjects();
fixtures.ThenNoProjectIsAvailable(response);
});

0 comments on commit 419cbf6

Please sign in to comment.