Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const gradRouter = require('./routes/grad');
const gradReportsRouter = require('./routes/gradReports');
const challengeReportsRouter = require('./routes/challengeReports');
const assessmentRouter = require('./routes/assessments');
const psiRouter = require('./routes/psiSelection');
const promMid = require('express-prometheus-middleware');
const messagePubSub = require('./messaging/message-pub-sub');
messagePubSub.init().then(() => {
Expand Down Expand Up @@ -223,6 +224,7 @@ apiRouter.use('/grad',gradRouter);
apiRouter.use('/gradReports', gradReportsRouter);
apiRouter.use('/challengeReports', challengeReportsRouter);
apiRouter.use('/assessments',assessmentRouter);
apiRouter.use('/psi', psiRouter);

//Handle 500 error
app.use((err, _req, res, next) => {
Expand Down
71 changes: 71 additions & 0 deletions backend/src/components/psiSelection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';
const {
getAccessToken,
getData,
handleExceptionResponse
} = require('./utils');
const HttpStatus = require('http-status-codes');
const config = require('../config');
const log = require('./logger');
const {doesSchoolBelongToDistrict} = require('./institute-cache');


async function downloadPsiSelectionReport(req, res) {
try {
console.log(req.params.schoolID);
if(req.session.activeInstituteType === 'DISTRICT'){
if(!doesSchoolBelongToDistrict(req.params.schoolID, req.session.activeInstituteIdentifier)){
return res.status(HttpStatus.CONFLICT).json({
status: HttpStatus.CONFLICT,
message: 'The school is not within your district. The report cannot be accessed.'
});
} else {
return res.status(HttpStatus.CONFLICT).json({
status: HttpStatus.CONFLICT,
message: 'This report is only available to schools.'
});
}
}else if(req.session.activeInstituteType === 'SCHOOL'){
if(req.params.schoolID !== req.session.activeInstituteIdentifier){
return res.status(HttpStatus.CONFLICT).json({
status: HttpStatus.CONFLICT,
message: 'Your school is not your school. The report cannot be accessed.'
});
}
}

const token = getAccessToken(req);

let url = `${config.get('psiSelection:rootURL')}/report/school/${req.params.schoolID}`;

const resData = await getData(token, url);
const fileDetails = getFileDetails('psi', resData?.reportName);
setResponseHeaders(res, fileDetails);

const buffer = Buffer.from(resData.documentData, 'base64');
return res.status(HttpStatus.OK).send(buffer);
} catch (e) {
log.error('downloadPsiReport Error', e.stack);
return handleExceptionResponse(e, res);
}
}

function setResponseHeaders(res, { filename, contentType }) {
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.setHeader('Content-Type', contentType);
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition, Content-Type');
}

function getFileDetails(reportType, reportName) {
if (reportType === 'psi') {
const base = (reportName || 'psi-selection-report').toString();
const safe = `${base}.csv`.replace(/[^\w .-]/g, '_');
return { filename: safe, contentType: 'text/csv; charset=utf-8' };
}
return { filename: 'download.csv', contentType: 'text/csv; charset=utf-8' };
}

module.exports = {
downloadPsiSelectionReport
};

3 changes: 3 additions & 0 deletions backend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ nconf.defaults({
assessmentSpecialCaseTypeCodeURL: process.env.ASSESSMENT_API_ENDPOINT+ '/assessment-specialcase-types',
assessmentStudentsURL: process.env.ASSESSMENT_API_ENDPOINT+ '/student',
},
psiSelection:{
rootURL: process.env.PSI_SELECTION_API_ENDPOINT,
},
challengeReports:{
rootURL: process.env.CHALLENGE_REPORTS_API_ENDPOINT
}
Expand Down
12 changes: 12 additions & 0 deletions backend/src/routes/psiSelection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { downloadPsiSelectionReport } = require('../components/psiSelection');
const auth = require('../components/auth');
const isValidBackendToken = auth.isValidBackendToken();
const { isValidUUIDParam, validateAccessToken, checkEdxUserPermission } = require('../components/permissionUtils');
const { PERMISSION } = require('../util/Permission');


router.get('/report/school/:schoolID', auth.refreshJWT, isValidBackendToken, validateAccessToken, isValidUUIDParam('schoolID'), checkEdxUserPermission(PERMISSION.EAS_SCH_VIEW), downloadPsiSelectionReport);

module.exports = router;
22 changes: 20 additions & 2 deletions frontend/src/components/graduation/school/GraduationSchoolTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
>
Current Students in GRAD System
</v-tab>
<v-tab
id="psiSelections"
value="psiSelections"
prepend-icon="mdi-account-school-outline"
>
PSI Selections
</v-tab>
</v-tabs>
</v-col>
</v-row>
Expand Down Expand Up @@ -81,6 +88,15 @@
:school-i-d="schoolID"
/>
</v-window-item>
<v-window-item
value="psiSelections"
transition="false"
reverse-transition="false"
>
<PsiSelections
:school-i-d="schoolID"
/>
</v-window-item>
</v-window>
</v-card-text>
</v-col>
Expand All @@ -96,6 +112,7 @@ import { mapState } from 'pinia';
import {ApiRoutes, PAGE_TITLES} from '../../../utils/constants';
import GradSchoolUploadDataComponent from './upload/GradSchoolUploadDataComponent.vue';
import GradReportsAndTranscripts from './reports/GradSchoolReportsAndTranscripts.vue';
import PsiSelections from './reports/PsiSelection.vue';
import GradSchoolStudentSearch from './students/GradSchoolStudentSearch.vue';
import GradSchoolCurrentStudents from './students/GradSchoolCurrentStudents.vue';
import ApiService from '../../../common/apiService';
Expand All @@ -107,7 +124,8 @@ export default {
GradSchoolCurrentStudents,
GradSchoolStudentSearch,
GradReportsAndTranscripts,
GradSchoolUploadDataComponent
GradSchoolUploadDataComponent,
PsiSelections
},
mixins: [alertMixin],
props: {
Expand All @@ -119,7 +137,7 @@ export default {
},
data() {
return {
validTabs: ['uploadData', 'studentSearch', 'gradReports', 'currentStudents'],
validTabs: ['uploadData', 'studentSearch', 'gradReports', 'currentStudents', 'psiSelections'],
PAGE_TITLES: PAGE_TITLES,
collectionObject: null
};
Expand Down
119 changes: 119 additions & 0 deletions frontend/src/components/graduation/school/reports/PsiSelection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<template>
<v-container fluid>
hello from PSI Selection
<DownloadLink
label="PSI Report"
:download-action="() => downloadPsiReport()"
/>
</v-container>
</template>

<script>
import DownloadLink from '../../../common/DownloadLink.vue';
import { mapState } from 'pinia';
import { authStore } from '../../../../store/modules/auth';
import alertMixin from '../../../../mixins/alertMixin';
import {ApiRoutes} from '../../../../utils/constants';
import ApiService from '../../../../common/apiService';

export default {
name: 'PsiSelection',
components: { DownloadLink },
mixins: [alertMixin],
props: {
schoolID: {
type: String,
required: false,
default: null
},
},
emits: [],
data() {
return {
isLoading: false,
};
},
computed: {
...mapState(authStore, ['userInfo']),
},
async created() {
},
methods: {
async downloadPsiReport() {
this.isLoading = true;
const url = `${ApiRoutes.psiSelection.REPORT}/school/${this.schoolID}`;

try {
const res = await ApiService.apiAxios.get(url, {
responseType: 'blob'
});

let filename = 'PSI Selection Report.csv';
const dispo = res.headers?.['content-disposition'] || res.headers?.['Content-Disposition'];
if (dispo) {
const m = dispo.match(/filename\*?=(?:UTF-8''|")(.*?)(?:"|;|$)/i);
if (m?.[1]) filename = decodeURIComponent(m[1]);
}

const urlObj = window.URL.createObjectURL(res.data);
const a = document.createElement('a');
a.style.display = 'none';
a.href = urlObj;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(urlObj);
} catch (error) {
console.error('Error downloading file:', error);
let errorMsg;

if (error.code === 'ERR_BAD_REQUEST') {
errorMsg = `PSI report not found for ${this.schoolID}`;
} else {
errorMsg = 'Error encountered while attempting to retrieve PSI report.';
}

this.setFailureAlert(errorMsg);
} finally {
this.isLoading = false;
}
}
},
};
</script>

<style scoped>

h3 {
color: #38598a;
}

button {
color: #1976d2;
}

v-text-field{
width: 4em;
}

ul {
list-style-type: none;
padding-top: 1em;
padding-bottom: 2em;
}

li {
padding-top: 1em;
}

p {
padding-top: 1em;
font-style: italic;
}

i {
font-size: 1.25em;
}

</style>
4 changes: 4 additions & 0 deletions frontend/src/components/util/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ export default {
return title.replace(/\s+/g, '');
},
isYukon() {
let isDistrict = this.userInfo.activeInstituteType === 'DISTRICT';
if (!isDistrict) {
return false;
}
let districtID = this.userInfo.activeInstituteIdentifier;
let belongsToDistrict = this.activeDistrictsMap.get(districtID);
return belongsToDistrict !== null && belongsToDistrict.districtNumber === '098';
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const studentRoot = baseRoot + '/students';
const penServicesRoot = baseRoot + '/penServices';
const assessmentsRoot = baseRoot + '/assessments';
const challengeReportsRoot = baseRoot + '/challengeReports';
const psiSelectionRoot = baseRoot + '/psi';

let object;

Expand Down Expand Up @@ -145,6 +146,10 @@ export const ApiRoutes = Object.freeze({
},
challengeReports: {
BASE_URL: challengeReportsRoot
},
psiSelection: {
BASE_URL: psiSelectionRoot,
REPORT: psiSelectionRoot + '/report',
}
});

Expand Down
4 changes: 2 additions & 2 deletions tools/config/update-configmap.sh

Large diffs are not rendered by default.

Loading