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

Assessment Keys upload UX. #2159

Merged
merged 7 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Expand Down Expand Up @@ -229,5 +252,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;
8 changes: 6 additions & 2 deletions backend/src/routes/eas.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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} = require('../validations/eas');
const {putStudentAssessmentSchema, fileUploadSchema} = require('../validations/eas');
const { scanFilePayload } = require('../components/fileUtils');

const PERMISSION = perm.PERMISSION;

Expand All @@ -22,6 +24,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 @@ -36,6 +36,19 @@ 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();

module.exports = {
putStudentAssessmentSchema,
fileUploadSchema
};
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
Loading