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

EDX-982: Edit an Independent School's Funding Group #1525

Merged
merged 2 commits into from
Aug 11, 2023
Merged
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 @@ -42,6 +42,7 @@ const analyticsRouter = require('./routes/analytics-router');
const nominalRollRouter = require('./routes/nominal-roll');
const edxRouter = require('./routes/edx-router');
const instituteRouter = require('./routes/institute');
const sdcRouter = require('./routes/sdc');
const cacheRouter = require('./routes/cache-router');
const promMid = require('express-prometheus-middleware');
const Redis = require('./util/redis/redis-client');
Expand Down Expand Up @@ -183,6 +184,7 @@ apiRouter.use('/analytics', analyticsRouter);
apiRouter.use('/nominal-roll', nominalRollRouter);
apiRouter.use('/edx', edxRouter);
apiRouter.use('/institute', instituteRouter);
apiRouter.use('/sdc', sdcRouter);
apiRouter.use('/cache', cacheRouter);
// Prevent unhandled errors from crashing application
process.on('unhandledRejection', err => {
Expand Down
173 changes: 173 additions & 0 deletions backend/src/components/sdc/sdc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use strict';
const { logApiError, getData, deleteData, errorResponse, getBackendToken, validateAccessToken, putData} = require('../utils');
const HttpStatus = require('http-status-codes');
const config = require('../../config');
const cacheService = require('../cache-service');
const utils = require('../utils');

async function getFundingGroupDataForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

let school = cacheService.getSchoolBySchoolID(req.params.schoolID);

if(!hasSchoolAdminRole(req, school)){
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'You do not have the required access for this function'
});
}

const data = await getData(accessToken, `${config.get('sdc:fundingGroupDataURL')}/search/${req.params.schoolID}`);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'getFundingGroupDataForSchool', 'Error getting funding data for this school');
return errorResponse(res);
}
}

async function getSnapshotFundingDataForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

let school = cacheService.getSchoolBySchoolID(req.params.schoolID);

if(!hasSchoolAdminRole(req, school)){
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'You do not have the required access for this function'
});
}

const data = await getData(accessToken, `${config.get('sdc:fundingGroupDataURL')}/snapshot/${req.params.schoolID}/${req.params.collectionID}`);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'getSnapshotFundingDataForSchool', 'Error getting funding snapshot data for this school');
return errorResponse(res);
}
}

async function getFundingGroupsForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

const data = await getData(accessToken, `${config.get('sdc:fundingGroupsURL')}`);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'getFundingGroupsForSchool', 'Error getting funding groups');
return errorResponse(res);
}
}

async function deleteFundingDataForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

let school = cacheService.getSchoolBySchoolID(req.params.schoolID);

if(!hasSchoolAdminRole(req, school)){
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'You do not have the required access for this function'
});
}

const data = await deleteData(accessToken, `${config.get('sdc:fundingGroupDataURL')}/${req.params.schoolFundingGroupID}`);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'deleteFundingDataForSchool', 'Error removing funding data for this school');
return errorResponse(res);
}
}

async function updateFundingDataForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

let school = cacheService.getSchoolBySchoolID(req.params.schoolID);

if(!hasSchoolAdminRole(req, school)){
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'You do not have the required access for this function'
});
}

const payload = req.body;
payload.updateDate = null;
payload.createDate = null;
payload.updateUser = utils.getUser(req).idir_username;

const data = await putData(accessToken, `${config.get('sdc:fundingGroupDataURL')}/${req.params.schoolFundingGroupID}`, payload);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'updateFundingDataForSchool', 'Error updating funding data for this school');
return errorResponse(res);
}
}

async function addNewFundingForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

let school = cacheService.getSchoolBySchoolID(req.params.schoolID);

if(!hasSchoolAdminRole(req, school)){
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'You do not have the required access for this function'
});
}

const payload = req.body;
payload.createUser = utils.getUser(req).idir_username;
payload.schoolID = req.params.schoolID;

const data = await utils.postData(accessToken, `${config.get('sdc:fundingGroupDataURL')}`, payload);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'addNewFundingForSchool', 'Error adding funding data for this school');
return errorResponse(res);
}
}

async function getAllCollectionsForSchool(req, res) {
try {
const accessToken = getBackendToken(req);
validateAccessToken(accessToken, res);

let school = cacheService.getSchoolBySchoolID(req.params.schoolID);

if(!hasSchoolAdminRole(req, school)){
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'You do not have the required access for this function'
});
}

const data = await getData(accessToken, `${config.get('sdc:schoolCollectionURL')}/searchAll/${req.params.schoolID}`);
return res.status(HttpStatus.OK).json(data);
} catch (e) {
logApiError(e, 'getFundingGroupDataForSchool', 'Error getting funding data for this school');
return errorResponse(res);
}
}

function hasSchoolAdminRole(req, school){
if(school.schoolCategoryCode === 'INDEPEND' || school.schoolCategoryCode === 'INDP_FNS'){
return req.session.roles.includes('SCHOOL_ADMIN') || req.session.roles.includes('SCHOOL_INDEPENDENT_ADMIN');
}

return req.session.roles.includes('SCHOOL_ADMIN');
}


module.exports = {
getFundingGroupsForSchool,
getFundingGroupDataForSchool,
deleteFundingDataForSchool,
updateFundingDataForSchool,
getSnapshotFundingDataForSchool,
addNewFundingForSchool,
getAllCollectionsForSchool
};
6 changes: 6 additions & 0 deletions backend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,11 @@ nconf.defaults({
bannerColor: process.env.BANNER_COLOR,
webSocketURL: process.env.WEB_SOCKET_URL
},
sdc: {
rootURL: process.env.SDC_API_URL,
schoolCollectionURL: process.env.SDC_API_URL + '/sdcSchoolCollection',
fundingGroupsURL: process.env.SDC_API_URL + '/funding-group-codes',
fundingGroupDataURL: process.env.SDC_API_URL + '/schoolFundingGroup'
}
});
module.exports = nconf;
19 changes: 19 additions & 0 deletions backend/src/routes/sdc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const passport = require('passport');
const express = require('express');
const router = express.Router();
const utils = require('../components/utils');
const extendSession = utils.extendSession();
const { getFundingGroupsForSchool, getFundingGroupDataForSchool, deleteFundingDataForSchool, updateFundingDataForSchool,
getSnapshotFundingDataForSchool, addNewFundingForSchool, getAllCollectionsForSchool} = require('../components/sdc/sdc');
const auth = require('../components/auth');

router.post('/funding-groups/:schoolID', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, addNewFundingForSchool);
router.get('/funding-groups/:schoolID', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, getFundingGroupDataForSchool);
router.delete('/funding-groups/:schoolID/funding/:schoolFundingGroupID', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, deleteFundingDataForSchool);
router.put('/funding-groups/:schoolID/funding/:schoolFundingGroupID', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, updateFundingDataForSchool);
router.get('/funding-groups/snapshot/:schoolID/:collectionID', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, getSnapshotFundingDataForSchool);

router.get('/funding-groups', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, getFundingGroupsForSchool);
router.get('/sdcSchoolCollection/:schoolID', passport.authenticate('jwt', {session: false}, undefined), auth.isLoggedInUser, extendSession, getAllCollectionsForSchool);
module.exports = router;

16 changes: 13 additions & 3 deletions frontend/src/components/institute/SchoolDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@
<v-tab value="moves">
Moves
</v-tab>
<v-tab value="funding">
<v-tab
v-if="canViewFundingTab()"
value="funding"
>
Funding
</v-tab>
</v-tabs>
Expand Down Expand Up @@ -177,7 +180,9 @@
/>
</v-window-item>
<v-window-item value="funding">
<p>Funding Tab</p>
<SchoolFunding
:school-i-d="schoolID"
/>
</v-window-item>
</v-window>
</v-card-text>
Expand All @@ -202,6 +207,7 @@ import Details from './common/Details.vue';
import SchoolHistory from './common/SchoolHistory.vue';
import SchoolContacts from './common/SchoolContacts.vue';
import SchoolMove from './common/SchoolMove.vue';
import SchoolFunding from './common/SchoolFunding.vue';
import {authStore} from '@/store/modules/auth';
import {notificationsStore} from '@/store/modules/notifications';
import InstituteNotes from '@/components/institute/common/InstituteNotes.vue';
Expand All @@ -213,7 +219,8 @@ export default {
Details,
SchoolHistory,
SchoolContacts,
SchoolMove
SchoolMove,
SchoolFunding
},
mixins: [alertMixin],
props: {
Expand Down Expand Up @@ -334,6 +341,9 @@ export default {
}
return this.SCHOOL_ADMIN_ROLE;
},
canViewFundingTab() {
return this.independentArray.includes(this.school.schoolCategoryCode);
},
saveNewSchoolNote(schoolNote) {
this.noteRequestCount += 1;
const payload = {
Expand Down
125 changes: 125 additions & 0 deletions frontend/src/components/institute/common/AddSchoolFunding.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<v-card
id="newFundingSheet"
class="information-window-v-card"
>
<v-card-title class="sheetHeader pt-1 pb-1">
New Funding Configuration
</v-card-title>
<v-divider />
<v-card-text>
<v-form
ref="newFundingForm"
v-model="isFormValid"
>
<v-row>
<v-col>
<v-select
v-model="selectedGradeCodeForFunding"
:items="filteredgradeCodes"
item-value="schoolGradeCode"
item-title="label"
label="Grades Offered"
variant="underlined"
required
:rules="[rules.required()]"
/>
</v-col>
<v-col>
<v-select
v-model="selectedFundingGroup"
:items="fundingGroups"
item-value="schoolFundingGroupCode"
item-title="label"
label="Funding Group"
variant="underlined"
required
:rules="[rules.required()]"
/>
</v-col>
</v-row>
<v-row class="py-4 pr-2 justify-end">
<PrimaryButton
id="cancel"
secondary
text="Cancel"
class="mr-2"
@click-action="closeAddFunding"
/>
<PrimaryButton
id="save"
text="Save"
width="7rem"
:disabled="!isFormValid"
@click-action="saveNewFundingData"
/>
</v-row>
</v-form>
</v-card-text>
</v-card>
</template>

<script>
import PrimaryButton from '../../util/PrimaryButton.vue';
import * as Rules from '@/utils/institute/formRules';

export default {
name: 'AddSchoolFunding',
components: {
PrimaryButton
},
props: {
schoolID: {
type: String,
required: true
},
fundingGroups: {
type: Array,
required: true
},
filteredgradeCodes: {
type: Array,
required: true
}
},
emits: ['closeAddFunding', 'saveNewFundingData'],
data() {
return {
isFormValid: false,
selectedGradeCodeForFunding: '',
selectedFundingGroup: '',
rules: Rules,
};
},
mounted() {
this.validateForm();
},
methods: {
closeAddFunding() {
this.$emit('closeAddFunding');
this.resetForm();
},
saveNewFundingData() {
this.$emit('saveNewFundingData', { schoolGradeCode: this.selectedGradeCodeForFunding, schoolFundingGroupCode: this.selectedFundingGroup });
this.resetForm();
},
resetForm() {
this.$refs.newFundingForm.reset();
this.validateForm();
},
validateForm() {
const isValid = this.$refs.newFundingForm.validate();
this.isFormValid = isValid.valid;
},
}
};
</script>

<style scoped>
.sheetHeader {
background-color: #003366;
color: white;
font-size: medium !important;
font-weight: bolder !important;
}
</style>
Loading
Loading