Skip to content

Commit

Permalink
Merge pull request #2159 from bcgov/feature/EAC-52
Browse files Browse the repository at this point in the history
Assessment Keys upload UX.
  • Loading branch information
eckermania authored Jan 6, 2025
2 parents 1aac7a0 + ae4aada commit 3380815
Show file tree
Hide file tree
Showing 18 changed files with 830 additions and 15 deletions.
7 changes: 7 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"atob": "2.1.2",
"axios": "^1.7.4",
"body-parser": "^1.19.0",
"clamdjs": "^1.0.2",
"config": "^3.3.3",
"connect-redis": "^5.1.0",
"cron": "^1.8.2",
Expand Down
30 changes: 27 additions & 3 deletions backend/src/components/eas/eas.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const config = require('../../config');
const cacheService = require('../cache-service');
const { createMoreFiltersSearchCriteria } = require('./studentFilters');
const moment = require('moment');
const {LocalDate, DateTimeFormatter} = require("@js-joda/core");
const {LocalDate, DateTimeFormatter} = require('@js-joda/core');
const log = require('../logger');

async function getAssessmentSessions(req, res) {
try {
Expand Down Expand Up @@ -136,7 +137,6 @@ async function postAssessmentStudent(req, res){
updateDate: null,
createDate: null
};
console.log("payload", payload)
const result = await utils.postData(`${config.get('server:eas:assessmentStudentsURL')}`, payload);
return res.status(HttpStatus.OK).json(result);
} catch (e) {
Expand Down Expand Up @@ -177,6 +177,29 @@ async function deleteAssessmentStudentByID(req, res) {
}
}

async function uploadAssessmentKeyFile(req, res) {
try {
const userInfo = utils.getUser(req);
let createUpdateUser = userInfo.idir_username;
const payload = {
fileContents: req.body.fileContents,
fileName: req.body.fileName,
fileType: req.body.fileType,
createUser: createUpdateUser,
updateUser: createUpdateUser
};
let data = await utils.postData(`${config.get('server:eas:assessmentKeyURL')}/${req.params.sessionID}/file`, payload, null, userInfo.idir_username);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
console.log(JSON.stringify(e));
if (e.status === 400) {
return res.status(HttpStatus.BAD_REQUEST).json(e.data.subErrors[0].message);
}
log.error('uploadAssessmentKeyFile Error', e.stack);
return handleExceptionResponse(e, res);
}
}

function includeAssessmentStudentProps(assessmentStudent) {
if(assessmentStudent) {
let school = cacheService.getSchoolBySchoolID(assessmentStudent.schoolID);
Expand Down Expand Up @@ -228,5 +251,6 @@ module.exports = {
updateAssessmentStudentByID,
deleteAssessmentStudentByID,
getAssessmentSpecialCases,
postAssessmentStudent
postAssessmentStudent,
uploadAssessmentKeyFile
};
56 changes: 56 additions & 0 deletions backend/src/components/fileUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';
const {createScanner} = require('clamdjs');
const config = require('../config');
const HttpStatus = require('http-status-codes');

async function scanFilePayload(req, res, next) {
const valid = await scanFile(req.body.documentData ? req.body.documentData : req.body.fileContents);

if (!valid) {
return res.status(HttpStatus.NOT_ACCEPTABLE).json({
status: HttpStatus.NOT_ACCEPTABLE,
message: 'File has failed the virus scan'
});
}

// no virus found in file
next();
}

async function scanSecureExchangeDocumentPayload(req, res, next) {
let documents = req.body.secureExchangeDocuments ? req.body.secureExchangeDocuments : [];
for (const document of documents) {
let valid = await scanFile(document?.documentData);
if (!valid) {
return res.status(HttpStatus.NOT_ACCEPTABLE).json({
status: HttpStatus.NOT_ACCEPTABLE,
message: 'File has failed the virus scan'
});
}
}
return next();
}

async function scanFile(base64File){
try{
const ClamAVScanner = createScanner(config.get('clamav:host'), Number(config.get('clamav:port')));
const clamAVScanResult = await ClamAVScanner.scanBuffer(Buffer.from(base64File, 'base64'), 3000, 1024 * 1024);
if (clamAVScanResult.includes('FOUND')) {
console.log('ClamAV scan found possible virus');
return false;
}
} catch (e) {
// if virus scan is not to be performed/cannot be performed
console.log('ClamAV Scanner was not found: ' + e);
}
console.log('ClamAV scan found no virus in file, allowing upload...');
return true;
}

const utils = {
scanFilePayload,
scanSecureExchangeDocumentPayload,
scanFile
};

module.exports = utils;
5 changes: 5 additions & 0 deletions backend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ nconf.defaults({
assessmentTypeCodeURL: process.env.EAS_API_URL+ '/assessment-types',
assessmentSpecialCaseTypeCodeURL: process.env.EAS_API_URL+ '/assessment-specialcase-types',
assessmentStudentsURL: process.env.EAS_API_URL+ '/student',
assessmentKeyURL: process.env.EAS_API_URL+ '/assessment-keys',
}
},
oidc: {
Expand Down Expand Up @@ -222,6 +223,10 @@ nconf.defaults({
programEligibilityTypeCodesURL: process.env.SDC_API_URL + '/program-eligibility-issue-codes',
zeroFteReasonCodesURL: process.env.SDC_API_URL + '/zero-fte-reason-codes',
sdcDuplicateURL: process.env.SDC_API_URL + '/sdc-duplicate'
},
clamav: {
host: process.env.CLAMAV_HOST,
port: process.env.CLAMAV_PORT,
}
});
module.exports = nconf;
9 changes: 7 additions & 2 deletions backend/src/routes/eas.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
const passport = require('passport');
const express = require('express');
const router = express.Router();
const { postAssessmentStudent, getAssessmentSessions, getAssessmentSessionsBySchoolYear, updateAssessmentSession, getAssessmentStudentsPaginated, getAssessmentStudentByID, updateAssessmentStudentByID, getAssessmentSpecialCases, deleteAssessmentStudentByID } = require('../components/eas/eas');
const { postAssessmentStudent, getAssessmentSessions, getAssessmentSessionsBySchoolYear, updateAssessmentSession, getAssessmentStudentsPaginated, getAssessmentStudentByID, updateAssessmentStudentByID, getAssessmentSpecialCases, deleteAssessmentStudentByID,
uploadAssessmentKeyFile } = require('../components/eas/eas');
const utils = require('../components/utils');
const extendSession = utils.extendSession();
const permUtils = require('../components/permissionUtils');
const perm = require('../util/Permission');
const validate = require('../components/validator');
const {putStudentAssessmentSchema, postStudentAssessmentSchema} = require('../validations/eas');

const {putStudentAssessmentSchema, postStudentAssessmentSchema, fileUploadSchema} = require('../validations/eas');
const { scanFilePayload } = require('../components/fileUtils');

const PERMISSION = perm.PERMISSION;

Expand All @@ -22,6 +25,8 @@ router.put('/assessment-registrations/student/:assessmentStudentID', passport.au
router.get('/assessment-registrations/paginated', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.VIEW_EAS_STUDENT_PERMISSION), extendSession, getAssessmentStudentsPaginated);
router.delete('/assessment-registrations/student/:assessmentStudentID', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.EDIT_EAS_STUDENT_PERMISSION), extendSession, deleteAssessmentStudentByID);

router.post('/assessment-keys/session/:sessionID/upload-file', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.MANAGE_EAS_ASSESSMENT_KEYS_PERMISSION), extendSession, validate(fileUploadSchema), scanFilePayload, uploadAssessmentKeyFile);

router.get('/assessment-specialcase-types', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.MANAGE_EAS_SESSIONS_PERMISSION), extendSession, getAssessmentSpecialCases);

module.exports = router;
4 changes: 2 additions & 2 deletions backend/src/util/Permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const PERMISSION = Object.freeze(
REPORTS_SDC_HEADCOUNTS_PERMISSION: 'REPORTS_SDC_HEADCOUNTS_PERMISSION',
MANAGE_EAS_SESSIONS_PERMISSION:'MANAGE_EAS_SESSIONS_PERMISSION',
VIEW_EAS_STUDENT_PERMISSION: 'VIEW_EAS_STUDENT_PERMISSION',
EDIT_EAS_STUDENT_PERMISSION: 'EDIT_EAS_STUDENT_PERMISSION'

EDIT_EAS_STUDENT_PERMISSION: 'EDIT_EAS_STUDENT_PERMISSION',
MANAGE_EAS_ASSESSMENT_KEYS_PERMISSION:'MANAGE_EAS_ASSESSMENT_KEYS_PERMISSION'
}
);

Expand Down
13 changes: 13 additions & 0 deletions backend/src/validations/eas.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ const putStudentAssessmentSchema = object({
query: object().noUnknown(),
}).noUnknown();

const fileUploadSchema = object({
body:object({
fileName: string().nonNullable(),
fileContents: string().nonNullable(),
fileType: string().nonNullable()
}).concat(baseRequestSchema).noUnknown(),
params: object({
sessionID: string().nonNullable()
}).noUnknown(),
query: object().noUnknown(),
}).noUnknown();

const postStudentAssessmentSchema = object({
body: object({
sessionID:string().nonNullable(),
Expand Down Expand Up @@ -74,5 +86,6 @@ const postStudentAssessmentSchema = object({

module.exports = {
putStudentAssessmentSchema,
fileUploadSchema,
postStudentAssessmentSchema
};
133 changes: 133 additions & 0 deletions frontend/src/components/assessments/AssessmentDataExchange.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<template>
<v-container class="containerSetup" :fluid="true">
<v-row class="d-flex justify-start">
<v-col>
<h2 class="subjectHeading">
School Year: {{ schoolYear?.replace("-", "/") }}
</h2>
</v-col>
</v-row>
<v-row no-gutters>
<v-col>
<v-divider class="divider" />
</v-col>
</v-row>
<v-row v-if="isLoading" class="mt-0">
<v-col>
<Spinner />
</v-col>
</v-row>
<v-row v-else>
<v-col class="border">
<v-tabs v-model="tab" color="#38598a">
<v-tab :value="1">Assessment Keys</v-tab>
<v-tab :value="2">Assessment Results</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item
:value="1"
transition="false"
reverse-transition="false"
>
<AssessmentKeyUpload
v-if="activeSessions?.length > 0"
:school-year="schoolYear"
:school-year-sessions="activeSessions"
/>
</v-window-item>
<v-window-item
:value="2"
transition="false"
reverse-transition="false"
/>
<v-window-item
:value="3"
transition="false"
reverse-transition="false"
/>
</v-window>
</v-col>
</v-row>
</v-container>
</template>
<script>
import Spinner from '@/components/common/Spinner.vue';
import ApiService from '../../common/apiService';
import { Routes } from '../../utils/constants';
import AssessmentKeyUpload from './data-exchange/AssessmentKeyUpload.vue';
import { DateTimeFormatter, LocalDate } from '@js-joda/core';
export default {
name: 'AssessmentSessionDetail',
components: {
Spinner,
AssessmentKeyUpload,
},
mixins: [],
props: {},
data() {
return {
activeSessions: [],
schoolYear: null,
isLoading: false,
tab: '',
};
},
computed: {},
created() {
this.loading = true;
this.getActiveSessions();
},
methods: {
async getActiveSessions() {
this.loading = true;
const formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
ApiService.apiAxios
.get(`${Routes.eas.EAS_ASSESSMENT_SESSIONS}`, {})
.then((response) => {
const allSessions = response.data;
allSessions.sort((a, b) => {
const dateA = LocalDate.parse(a.activeUntilDate, formatter);
const dateB = LocalDate.parse(b.activeUntilDate, formatter);
return dateB.compareTo(dateA);
});
this.schoolYear = allSessions?.length > 0 ? allSessions[0].schoolYear : null;
this.activeSessions = allSessions.filter((session) => session.schoolYear === this.schoolYear);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
this.loading = false;
});
},
backToAssesmentSessions() {
this.$router.push({ name: 'assessment-sessions' });
},
},
};
</script>
<style scoped>
.border {
border: 2px solid grey;
border-radius: 5px;
padding: 35px;
margin-bottom: 2em;
}
.divider {
border-color: #fcba19;
border-width: 3px;
opacity: unset;
}
.containerSetup {
padding-right: 5em !important;
padding-left: 5em !important;
}
@media screen and (max-width: 1200px) {
.containerSetup {
padding-right: 3em !important;
padding-left: 3em !important;
}
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/components/assessments/AssessmentSessions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default {
getAllAssessmentSessions() {
this.loading = true;
ApiService.apiAxios
.get(`${Routes.eas.GET_ASSESSMENT_SESSIONS}`, {})
.get(`${Routes.eas.EAS_ASSESSMENT_SESSIONS}`, {})
.then((response) => {
const formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default {
this.loading = true;
ApiService.apiAxios
.get(
`${Routes.eas.GET_ASSESSMENT_SESSIONS}/school-year/` +
`${Routes.eas.EAS_ASSESSMENT_SESSIONS}/school-year/` +
this.schoolYear,
{}
)
Expand Down
Loading

0 comments on commit 3380815

Please sign in to comment.