generated from ministryofjustice/hmpps-template-typescript
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #691 from ministryofjustice/APG-250-add-course
(APG-250) Add Course form page
- Loading branch information
Showing
9 changed files
with
245 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import type { DeepMocked } from '@golevelup/ts-jest' | ||
import { createMock } from '@golevelup/ts-jest' | ||
import type { NextFunction, Request, Response } from 'express' | ||
|
||
import AddCourseController from './addCourseController' | ||
import { findPaths } from '../../paths' | ||
import type { CourseService } from '../../services' | ||
import { audienceFactory, courseFactory } from '../../testutils/factories' | ||
import { CourseUtils } from '../../utils' | ||
import type { GovukFrontendSelectItem } from '@govuk-frontend' | ||
|
||
jest.mock('../../utils/courseUtils') | ||
|
||
const mockCourseUtils = CourseUtils as jest.Mocked<typeof CourseUtils> | ||
|
||
describe('AddCourseController', () => { | ||
let controller: AddCourseController | ||
let request: Request | ||
let response: Response | ||
|
||
const username = 'SOME_USERNAME' | ||
const userToken = 'USER_TOKEN' | ||
const courseService = createMock<CourseService>({}) | ||
const next: DeepMocked<NextFunction> = createMock<NextFunction>({}) | ||
|
||
beforeEach(() => { | ||
controller = new AddCourseController(courseService) | ||
request = createMock<Request>({ user: { token: userToken, username } }) | ||
response = createMock<Response>() | ||
}) | ||
|
||
describe('show', () => { | ||
const audienceSelectItems: Array<GovukFrontendSelectItem> = [ | ||
{ text: 'Audience 1', value: '1' }, | ||
{ text: 'Audience 2', value: '2' }, | ||
{ text: 'Audience 3', value: '3' }, | ||
] | ||
|
||
beforeEach(() => { | ||
mockCourseUtils.audienceSelectItems.mockReturnValue(audienceSelectItems) | ||
}) | ||
|
||
it('renders the create course form template with audience select items', async () => { | ||
const audiences = audienceFactory.buildList(3) | ||
courseService.getCourseAudiences.mockResolvedValue(audiences) | ||
|
||
const requestHandler = controller.show() | ||
await requestHandler(request, response, next) | ||
|
||
expect(courseService.getCourseAudiences).toHaveBeenCalledWith(userToken) | ||
|
||
expect(response.render).toHaveBeenCalledWith('courses/form/show', { | ||
audienceSelectItems, | ||
pageHeading: 'Add a Programme', | ||
}) | ||
expect(CourseUtils.audienceSelectItems).toHaveBeenCalledWith(audiences) | ||
}) | ||
}) | ||
|
||
describe('submit', () => { | ||
it('creates a course and redirects to the newly created course page', async () => { | ||
const audience = audienceFactory.build() | ||
const newCourseBody: Record<string, string> = { | ||
alternateName: 'TC+1', | ||
audienceId: audience.id, | ||
description: 'Test course description', | ||
name: 'Test Course', | ||
withdrawn: 'false', | ||
} | ||
const createdCourse = courseFactory.build({ | ||
alternateName: newCourseBody.alternateName, | ||
audience: audience.name, | ||
audienceColour: audience.colour, | ||
description: newCourseBody.description, | ||
name: newCourseBody.name, | ||
}) | ||
|
||
courseService.createCourse.mockResolvedValue(createdCourse) | ||
|
||
request.body = newCourseBody | ||
|
||
const requestHandler = controller.submit() | ||
await requestHandler(request, response, next) | ||
|
||
expect(courseService.createCourse).toHaveBeenCalledWith(username, { ...newCourseBody, withdrawn: false }) | ||
expect(response.redirect).toHaveBeenCalledWith(findPaths.show({ courseId: createdCourse.id })) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { Request, Response, TypedRequestHandler } from 'express' | ||
|
||
import { findPaths } from '../../paths' | ||
import type { CourseService } from '../../services' | ||
import { CourseUtils, TypeUtils } from '../../utils' | ||
|
||
export default class AddCourseController { | ||
constructor(private readonly courseService: CourseService) {} | ||
|
||
show(): TypedRequestHandler<Request, Response> { | ||
return async (req: Request, res: Response) => { | ||
TypeUtils.assertHasUser(req) | ||
|
||
const audiences = await this.courseService.getCourseAudiences(req.user.token) | ||
|
||
res.render('courses/form/show', { | ||
audienceSelectItems: CourseUtils.audienceSelectItems(audiences), | ||
pageHeading: 'Add a Programme', | ||
}) | ||
} | ||
} | ||
|
||
submit(): TypedRequestHandler<Request, Response> { | ||
return async (req: Request, res: Response) => { | ||
TypeUtils.assertHasUser(req) | ||
|
||
const { audienceId, name, alternateName, description, withdrawn } = req.body as Record<string, string> | ||
|
||
const createdCourse = await this.courseService.createCourse(req.user.username, { | ||
alternateName, | ||
audienceId, | ||
description, | ||
name, | ||
withdrawn: withdrawn === 'true', | ||
}) | ||
|
||
res.redirect(findPaths.show({ courseId: createdCourse.id })) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { Router } from 'express' | ||
|
||
import type { Controllers } from '../controllers' | ||
import { ApplicationRoles } from '../middleware' | ||
import { findPaths } from '../paths' | ||
import { RouteUtils } from '../utils' | ||
|
||
export default function routes(controllers: Controllers, router: Router): Router { | ||
const { get, post } = RouteUtils.actions(router, { allowedRoles: [ApplicationRoles.ACP_EDITOR] }) | ||
const { addCourseController } = controllers | ||
|
||
get(findPaths.course.add.show.pattern, addCourseController.show()) | ||
post(findPaths.course.add.create.pattern, addCourseController.submit()) | ||
|
||
return router | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
{% from "govuk/components/button/macro.njk" import govukButton %} | ||
{% from "govuk/components/input/macro.njk" import govukInput %} | ||
{% from "govuk/components/radios/macro.njk" import govukRadios %} | ||
{% from "govuk/components/select/macro.njk" import govukSelect %} | ||
{% from "govuk/components/textarea/macro.njk" import govukTextarea %} | ||
|
||
{% extends "../../partials/layout.njk" %} | ||
|
||
{% block content %} | ||
<h1 class="govuk-heading-l">{{ pageHeading }}</h1> | ||
|
||
<div class="govuk-grid-row"> | ||
<div class="govuk-grid-column-two-thirds"> | ||
<form method="post"> | ||
<input type="hidden" name="_csrf" value="{{ csrfToken }}"/> | ||
|
||
{{ govukSelect({ | ||
id: "programme-audience", | ||
name: "audienceId", | ||
label: { | ||
text: "Audience" | ||
}, | ||
items: audienceSelectItems, | ||
attributes: { | ||
"data-testid": "audience-select" | ||
}, | ||
errorMessage: errors.messages.audience | ||
}) }} | ||
|
||
{{ govukInput({ | ||
label: { | ||
text: "Programme name" | ||
}, | ||
id: "programme-name", | ||
name: "name", | ||
errorMessage: errors.messages.name | ||
}) }} | ||
|
||
{{ govukInput({ | ||
label: { | ||
text: "Alternative name (optional)" | ||
}, | ||
classes: "govuk-input--width-10", | ||
id: "programme-alternate-name", | ||
name: "alternateName", | ||
hint: { | ||
text: "Usually an abbreviation or acronym for the programme name" | ||
}, | ||
errorMessage: errors.messages.alternateName | ||
}) }} | ||
|
||
{{ govukTextarea({ | ||
name: "description", | ||
id: "programme-description", | ||
label: { | ||
text: "Description" | ||
} | ||
}) }} | ||
|
||
{{ govukRadios({ | ||
name: "withdrawn", | ||
fieldset: { | ||
legend: { | ||
text: "Withdrawn" | ||
} | ||
}, | ||
items: [ | ||
{ | ||
value: "true", | ||
text: "Yes" | ||
}, | ||
{ | ||
value: "false", | ||
text: "No" | ||
} | ||
] | ||
}) }} | ||
|
||
{{ govukButton({ text: "Submit" }) }} | ||
</form> | ||
</div> | ||
</div> | ||
{% endblock content %} |