Skip to content

Commit

Permalink
Merge branch 'main' into feature-branch/data-quality
Browse files Browse the repository at this point in the history
  • Loading branch information
duncanc19 committed Jan 13, 2025
2 parents aec1e18 + b5aa092 commit 6d3b715
Show file tree
Hide file tree
Showing 33 changed files with 2,438 additions and 1,428 deletions.
44 changes: 43 additions & 1 deletion backend/server/routes/establishments/mandatoryTraining/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,63 @@ const { hasPermission } = require('../../../utils/security/hasPermission');

const viewMandatoryTraining = async (req, res) => {
const establishmentId = req.establishmentId;
const previousAllJobsLengths = [29, 31, 32];

try {
const allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId);
let allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId);
let duplicateJobRolesUpdated = false;

if (allMandatoryTrainingRecords?.mandatoryTraining?.length) {
for (mandatoryTrainingCategory of allMandatoryTrainingRecords.mandatoryTraining) {
if (
hasDuplicateJobs(mandatoryTrainingCategory.jobs) &&
previousAllJobsLengths.includes(mandatoryTrainingCategory.jobs.length)
) {
duplicateJobRolesUpdated = true;

const thisMandatoryTrainingRecord = new MandatoryTraining(establishmentId);
await thisMandatoryTrainingRecord.load({
trainingCategoryId: mandatoryTrainingCategory.trainingCategoryId,
allJobRoles: true,
jobs: [],
});
await thisMandatoryTrainingRecord.save(req.userUid);
}
}

if (duplicateJobRolesUpdated) {
allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId);
}
}

return res.status(200).json(allMandatoryTrainingRecords);
} catch (err) {
console.error(err);
return res.status(500).send();
}
};

const hasDuplicateJobs = (jobRoles) => {
const seenJobIds = new Set();

for (const jobRole of jobRoles) {
if (seenJobIds.has(jobRole.id)) {
return true; // Duplicate found
}
seenJobIds.add(jobRole.id);
}

return false; // No duplicates found
};

/**
* Handle GET request for getting all saved mandatory training for view all mandatory training
*/
const viewAllMandatoryTraining = async (req, res) => {
const establishmentId = req.establishmentId;
try {
const allMandatoryTrainingRecords = await MandatoryTraining.fetchAllMandatoryTrainings(establishmentId);

return res.status(200).json(allMandatoryTrainingRecords);
} catch (err) {
console.error(err);
Expand Down Expand Up @@ -93,3 +134,4 @@ router.route('/:categoryId').delete(deleteMandatoryTrainingById);
module.exports = router;
module.exports.createAndUpdateMandatoryTraining = createAndUpdateMandatoryTraining;
module.exports.deleteMandatoryTrainingById = deleteMandatoryTrainingById;
module.exports.viewMandatoryTraining = viewMandatoryTraining;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const sinon = require('sinon');
const {
createAndUpdateMandatoryTraining,
deleteMandatoryTrainingById,
viewMandatoryTraining,
} = require('../../../../../routes/establishments/mandatoryTraining/index.js');

const MandatoryTraining = require('../../../../../models/classes/mandatoryTraining').MandatoryTraining;
Expand Down Expand Up @@ -34,6 +35,7 @@ describe('mandatoryTraining/index.js', () => {
expect(res.statusCode).to.deep.equal(500);
});
});

describe('createAndUpdateMandatoryTraining', () => {
it('should save the record for mandatory training if isvalid , not exists and all job role is selected', async () => {
sinon.stub(MandatoryTraining.prototype, 'load').callsFake(() => {
Expand Down Expand Up @@ -84,4 +86,120 @@ describe('mandatoryTraining/index.js', () => {
expect(res.statusCode).to.deep.equal(500);
});
});

describe('viewMandatoryTraining', () => {
let req;
let res;

beforeEach(() => {
req = httpMocks.createRequest();
req.establishmentId = 'mockId';
req.userUid = 'abc123';
res = httpMocks.createResponse();
});

const createMockFetchData = () => {
return {
allJobRolesCount: 37,
lastUpdated: '2025-01-03T11:55:55.734Z',
mandatoryTraining: [
{
category: 'Activity provision, wellbeing',
establishmentId: 100,
jobs: [{ id: 22, title: 'Registered manager' }],
trainingCategoryId: 1,
},
],
mandatoryTrainingCount: 1,
};
};

it('should fetch all mandatory training for establishment passed in request', async () => {
const fetchSpy = sinon.stub(MandatoryTraining, 'fetch').callsFake(() => createMockFetchData());

await viewMandatoryTraining(req, res);
expect(res.statusCode).to.deep.equal(200);
expect(fetchSpy).to.have.been.calledWith(req.establishmentId);
});

it('should return data from fetch and 200 status if fetch successful', async () => {
const mockFetchData = createMockFetchData();
sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData);

await viewMandatoryTraining(req, res);
expect(res.statusCode).to.equal(200);
expect(res._getJSONData()).to.deep.equal(mockFetchData);
});

it('should return 500 status if error when fetching data', async () => {
sinon.stub(MandatoryTraining, 'fetch').throws('Unexpected error');

await viewMandatoryTraining(req, res);
expect(res.statusCode).to.equal(500);
});

describe('Handling duplicate job roles', () => {
it('should not call load or save for mandatory training when no duplicate job roles in retrieved mandatory training', async () => {
const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load');
const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save');

sinon.stub(MandatoryTraining, 'fetch').callsFake(() => createMockFetchData());

await viewMandatoryTraining(req, res);
expect(loadSpy).not.to.have.been.called;
expect(saveSpy).not.to.have.been.called;
});

const previousAllJobsLengths = [29, 31, 32];

previousAllJobsLengths.forEach((allJobsLength) => {
it(`should call load and save for mandatory training instance when duplicate job roles in retrieved mandatory training and has length of previous all job roles (${allJobsLength})`, async () => {
const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load');
const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save');

const mockFetchData = createMockFetchData();

const mockJobRoles = Array.from({ length: allJobsLength - 1 }, (_, index) => ({
id: index + 1,
title: `Job role ${index + 1}`,
}));
mockJobRoles.push({ id: 1, title: 'Job role 1' });
mockFetchData.mandatoryTraining[0].jobs = mockJobRoles;

const fetchSpy = sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData);

await viewMandatoryTraining(req, res);

expect(loadSpy).to.have.been.calledWith({
trainingCategoryId: mockFetchData.mandatoryTraining[0].trainingCategoryId,
allJobRoles: true,
jobs: [],
});
expect(saveSpy).to.have.been.calledWith(req.userUid);
expect(fetchSpy).to.have.been.calledTwice;
});
});

it('should not call load or save for mandatory training when number of jobs is previous all job roles length (29) but no duplicate job roles', async () => {
const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load');
const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save');

const mockFetchData = createMockFetchData();

const mockJobRoles = Array.from({ length: 29 }, (_, index) => ({
id: index + 1,
title: `Job role ${index + 1}`,
}));

mockFetchData.mandatoryTraining[0].jobs = mockJobRoles;

sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData);

await viewMandatoryTraining(req, res);

expect(loadSpy).not.to.have.been.called;
expect(saveSpy).not.to.have.been.called;
});
});
});
});
3 changes: 2 additions & 1 deletion frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { PreviousRouteService } from '@core/services/previous-route.service';
import { QualificationService } from '@core/services/qualification.service';
import { RecruitmentService } from '@core/services/recruitment.service';
import { RegistrationService } from '@core/services/registration.service';
import { TrainingService } from '@core/services/training.service';
import { MandatoryTrainingService, TrainingService } from '@core/services/training.service';
import { windowProvider, WindowToken } from '@core/services/window';
import { WindowRef } from '@core/services/window.ref';
import { WorkerService } from '@core/services/worker.service';
Expand Down Expand Up @@ -193,6 +193,7 @@ import { SentryErrorHandler } from './SentryErrorHandler.component';
RegistrationService,
{ provide: ErrorHandler, useClass: SentryErrorHandler },
TrainingService,
MandatoryTrainingService,
WindowRef,
WorkerService,
InternationalRecruitmentService,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/app/core/model/training.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ export interface mandatoryJobs {

export interface mandatoryTraining {
trainingCategoryId: number;
allJobRoles: boolean;
allJobRoles?: boolean;
selectedJobRoles?: boolean;
category?: string;
jobs: mandatoryJobs[];
}
export interface allMandatoryTrainingCategories {
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/app/core/services/training.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { mandatoryTraining } from '@core/model/establishment.model';
import { allMandatoryTrainingCategories, SelectedTraining, TrainingCategory } from '@core/model/training.model';
import { Worker } from '@core/model/worker.model';
import { BehaviorSubject, Observable } from 'rxjs';
Expand Down Expand Up @@ -125,3 +126,31 @@ export class TrainingService {
this.updatingSelectedStaffForMultipleTraining = null;
}
}

export class MandatoryTrainingService extends TrainingService {
_onlySelectedJobRoles: boolean = null;
_mandatoryTrainingBeingEdited: mandatoryTraining = null;
public allJobRolesCount: number;

public get onlySelectedJobRoles(): boolean {
return this._onlySelectedJobRoles;
}

public set onlySelectedJobRoles(onlySelected: boolean) {
this._onlySelectedJobRoles = onlySelected;
}

public resetState(): void {
this.onlySelectedJobRoles = null;
this.mandatoryTrainingBeingEdited = null;
super.resetState();
}

public set mandatoryTrainingBeingEdited(mandatoryTraining) {
this._mandatoryTrainingBeingEdited = mandatoryTraining;
}

public get mandatoryTrainingBeingEdited(): mandatoryTraining {
return this._mandatoryTrainingBeingEdited;
}
}
17 changes: 17 additions & 0 deletions frontend/src/app/core/test-utils/MockRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable()
export class MockRouter extends Router {
public static factory(overrides: any = {}) {
return () => {
const service = new MockRouter();

Object.keys(overrides).forEach((overrideName) => {
service[overrideName] = overrides[overrideName];
});

return service;
};
}
}
77 changes: 50 additions & 27 deletions frontend/src/app/core/test-utils/MockTrainingService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { allMandatoryTrainingCategories, TrainingCategory } from '@core/model/training.model';
import { TrainingService } from '@core/services/training.service';
import { MandatoryTrainingService, TrainingService } from '@core/services/training.service';
import { Observable, of } from 'rxjs';

import { AllJobs, JobsWithDuplicates } from '../../../mockdata/jobs';
Expand Down Expand Up @@ -40,37 +40,16 @@ export class MockTrainingService extends TrainingService {
}

public getAllMandatoryTrainings(): Observable<allMandatoryTrainingCategories> {
return of({
allJobRolesCount: 37,
lastUpdated: new Date(),
mandatoryTraining: [
{
trainingCategoryId: 123,
allJobRoles: false,
category: 'Autism',
selectedJobRoles: true,
jobs: [
{
id: 15,
title: 'Activities worker, coordinator',
},
],
},
{
trainingCategoryId: 9,
allJobRoles: true,
category: 'Coshh',
selectedJobRoles: true,
jobs: this._duplicateJobRoles ? JobsWithDuplicates : AllJobs,
},
],
mandatoryTrainingCount: 2,
});
return of(mockMandatoryTraining(this._duplicateJobRoles));
}

public deleteCategoryById(establishmentId, categoryId) {
return of({});
}

public deleteAllMandatoryTraining(establishmentId) {
return of({});
}
}

@Injectable()
Expand Down Expand Up @@ -99,3 +78,47 @@ export class MockTrainingServiceWithPreselectedStaff extends MockTrainingService
};
}
}

@Injectable()
export class MockMandatoryTrainingService extends MandatoryTrainingService {
public static factory(overrides = {}) {
return (http: HttpClient) => {
const service = new MockMandatoryTrainingService(http);

Object.keys(overrides).forEach((overrideName) => {
service[overrideName] = overrides[overrideName];
});

return service;
};
}
}

export const mockMandatoryTraining = (duplicateJobRoles = false) => {
return {
allJobRolesCount: 37,
lastUpdated: new Date(),
mandatoryTraining: [
{
trainingCategoryId: 123,
allJobRoles: false,
category: 'Autism',
selectedJobRoles: true,
jobs: [
{
id: 15,
title: 'Activities worker, coordinator',
},
],
},
{
trainingCategoryId: 9,
allJobRoles: true,
category: 'Coshh',
selectedJobRoles: true,
jobs: duplicateJobRoles ? JobsWithDuplicates : AllJobs,
},
],
mandatoryTrainingCount: 2,
};
};
Loading

0 comments on commit 6d3b715

Please sign in to comment.