From 0b28d770846894a9b305e26db71c9bf8a2a8f96c Mon Sep 17 00:00:00 2001 From: Shaun Lum Date: Thu, 26 Oct 2023 08:38:16 -0700 Subject: [PATCH 1/2] school extract updates --- backend/src/app.js | 2 + backend/src/components/cache.js | 3 +- backend/src/components/utils.js | 93 +++++++++++++++++++++- backend/src/routes/download-router.js | 16 +++- backend/src/routes/institute-router.js | 43 +++++++++- frontend/src/components/DistrictSelect.vue | 4 - frontend/src/components/SchoolSelect.vue | 14 ++-- 7 files changed, 156 insertions(+), 19 deletions(-) diff --git a/backend/src/app.js b/backend/src/app.js index e2893f6d..2c442a64 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -13,6 +13,7 @@ const districtRouter = require("./routes/district-router"); const downloadRouter = require("./routes/download-router"); const authorityRouter = require("./routes/authority-router"); const offshoreRouter = require("./routes/offshore-router"); +const schoolRouter = require("./routes/school-router"); const app = express(); const publicPath = path.join(__dirname, "public"); @@ -76,6 +77,7 @@ apiRouter.use("/v1/institute", instituteRouter); apiRouter.use("/v1/district", districtRouter); apiRouter.use("/v1/authority", authorityRouter); apiRouter.use("/v1/offshore", offshoreRouter); +apiRouter.use("/v1/school", schoolRouter); //Handle 500 error app.use((err, _req, res, next) => { diff --git a/backend/src/components/cache.js b/backend/src/components/cache.js index eb85035f..c88473d4 100644 --- a/backend/src/components/cache.js +++ b/backend/src/components/cache.js @@ -1,6 +1,7 @@ const NodeCache = require("node-cache"); const listCache = new NodeCache({ stdTTL: 21600 }); +const schoolCache = new NodeCache({ stdTTL: 21600 }); module.exports = { - listCache + listCache,schoolCache }; \ No newline at end of file diff --git a/backend/src/components/utils.js b/backend/src/components/utils.js index 985ed6f5..3d1cff48 100644 --- a/backend/src/components/utils.js +++ b/backend/src/components/utils.js @@ -20,9 +20,18 @@ const ALLOWED_FILENAMES = new Set([ 'chairperson', 'secretary-treasurer', 'executive-admin-assistant', - 'exceldistrictcontacts' + 'exceldistrictcontacts', + 'excelschoolcontacts' // Add more allowed filepaths as needed ]); +const ALLOWED_SCHOOLCATEGORYCODES = new Set([ + 'PUBLIC', + 'INDEPEND' + // Add more allowed filepaths as needed +]); +function isAllowedSchoolCategory(category) { + return ALLOWED_SCHOOLCATEGORYCODES.has(category); +} function isSafeFilePath(filepath) { return ALLOWED_FILENAMES.has(filepath); } @@ -90,5 +99,83 @@ function addDistrictLabels(jsonData, districtList) { } return 0; } - - module.exports = { createList, isSafeFilePath, addDistrictLabels, districtNumberSort }; \ No newline at end of file + function createSchoolCache(schoolData, schoolGrades) { + const propertyOrder = ["mincode", "schoolNumber","mailing_addressLine1","mailing_addressLine2","mailing_postal","mailing_provinceCode","mailing_countryCode"]; + + // Preload convertedGrades with schoolGrades.schoolGradeCode and set the value to "N" + const convertedGrades = {}; + schoolGrades.forEach((grade) => { + convertedGrades[grade.schoolGradeCode] = "N"; + }); + + // Map over each school object + return schoolData.map((school) => { + const addressFields = { + mailing: {}, + physical: {}, + }; + + // Loop through the grades and set the value to "Y" for each grade + school.grades.forEach((grade) => { + convertedGrades[grade.schoolGradeCode] = "Y"; + }); + + // Extract and format principal contact information if it exists + const principalContact = school.contacts.find((contact) => contact.schoolContactTypeCode === "PRINCIPAL"); + if (principalContact) { + school.firstName = principalContact.firstName; + school.lastName = principalContact.lastName; + school.email = principalContact.email; + school.phoneNumber = principalContact.phoneNumber; + } + + // Loop through addresses and update the fields based on addressTypeCode + school.addresses.forEach((address) => { + if (address.addressTypeCode === "MAILING") { + Object.keys(address).forEach((field) => { + // Exclude the specified fields + if (![ + "createUser", + "updateUser", + "createDate", + "updateDate", + "schoolAddressId", + "schoolId", + "addressTypeCode" + ].includes(field)) { + addressFields.mailing[`mailing_${field}`] = address[field]; + } + }); + } else if (address.addressTypeCode === "PHYSICAL") { + Object.keys(address).forEach((field) => { + if (![ + "createUser", + "updateUser", + "createDate", + "updateDate", + "schoolAddressId", + "schoolId", + "addressTypeCode" + ].includes(field)) { + addressFields.mailing[`physical_${field}`] = address[field]; + } + }); + } + }); + + // Concatenate neighborhoodLearningTypeCode into a single string + const nlc = school.neighborhoodLearning.map(learning => learning.neighborhoodLearningTypeCode).join(' | '); + + // Create a new object with the properties rearranged + const rearrangedSchool = {}; + propertyOrder.forEach((prop) => { + rearrangedSchool[prop] = school[prop]; + }); + + // Merge the address fields and nlc into the school object + Object.assign(rearrangedSchool, convertedGrades, addressFields.mailing, addressFields.physical, { nlc }); + + return rearrangedSchool; + }); +} + module.exports = { createList, isSafeFilePath,isAllowedSchoolCategory, addDistrictLabels, districtNumberSort, createSchoolCache }; \ No newline at end of file diff --git a/backend/src/routes/download-router.js b/backend/src/routes/download-router.js index 95b0a78f..f029cf58 100644 --- a/backend/src/routes/download-router.js +++ b/backend/src/routes/download-router.js @@ -51,7 +51,10 @@ async function addDistrictLabels(req, res, next) { districtList= listCache.get("districtList"); } else { try { - const response = await axios.get('http://localhost:8080/api/v1/institute/district/list', { headers: { Authorization: `Bearer ${req.accessToken}` } }); + const path = "/api/v1/institute/district/list" + const url = `${req.protocol}://${req.hostname}:8080${path}`; + + const response = await axios.get(url, { headers: { Authorization: `Bearer ${req.accessToken}` } }); const districts = response.data; listCache.set("districtList", districts); districtList = districts @@ -99,10 +102,17 @@ async function getDownload(req, res,next){ return res.sendFile(filePath); }else{ try { - const url = `${config.get('server:instituteAPIURL')}` + req.url.replace('/csv', ''); + const path = req.url.replace('/csv', ''); // Modify the URL path as needed + const url = `${req.protocol}://${req.hostname}:8080/api/v1${path}`; + console.log(url) const response = await axios.get(url, { headers: { Authorization: `Bearer ${req.accessToken}` } }); // Attach the fetched data to the request object - req.jsonData = response.data.content; + if (response.data?.content) { + req.jsonData = response.data.content; + }else{ + req.jsonData = response.data; + } + next(); // Call the next middleware } catch (error) { console.error("Error:", error); diff --git a/backend/src/routes/institute-router.js b/backend/src/routes/institute-router.js index 3b4d5b8f..84f428ae 100644 --- a/backend/src/routes/institute-router.js +++ b/backend/src/routes/institute-router.js @@ -5,16 +5,33 @@ const config = require("../config/index"); const axios = require("axios"); const { checkToken } = require("../components/auth"); -const { createList, addDistrictLabels, districtNumberSort } = require("../components/utils"); +const { createList, addDistrictLabels, districtNumberSort, isAllowedSchoolCategory } = require("../components/utils"); const { listCache } = require("../components/cache"); const schoolListOptions = { fields: ["mincode", "displayName", "schoolId"], fieldToInclude: "closedDate", valueToInclude: null, sortField: "mincode" }; const districtListOptions = { fields: ["displayName", "districtId","districtNumber"] ,fieldToInclude: "districtStatusCode", valueToInclude: "ACTIVE", sortField: "districtNumber"}; const authorityListOptions = { fields: ["displayName", "authorityNumber","independentAuthorityId"], fieldToInclude: "closedDate", valueToInclude: null, sortField: "authorityNumber" }; +const openSchoolListOptions = { fields: [ + "schoolId", + "districtId", + "mincode", + "schoolNumber", + "faxNumber", + "phoneNumber", + "email", + "website", + "displayName", + "schoolCategoryCode", + "facilityTypeCode", + "openedDate", + "closedDate", + "districtNumber" +],fieldToInclude: "closedDate", valueToInclude: null, sortField: "mincode" }; //Batch Routes router.get("/contact-type-codes", checkToken, getContactTypeCodes); router.get("/offshore-school/list", checkToken, getOffshoreSchoolList); +// router.get("/school", checkToken, getOpenSchools); router.get("/school/list", checkToken, getSchoolList); router.get("/authority/list", checkToken, getAuthorityList); router.get("/district/list", checkToken, getDistrictList); @@ -123,6 +140,30 @@ async function getAuthorityList(req, res) { res.json(authorityList); } } +async function getOpenSchools(req, res) { + + + if (await !listCache.has("openschoollist")) { + const url = `${config.get("server:instituteAPIURL")}/institute/school`; // Update the URL according to your API endpoint + axios + .get(url, { headers: { Authorization: `Bearer ${req.accessToken}` } }) + .then((response) => { + const openSchoolList = createList(response.data, openSchoolListOptions); + res.json(openSchoolList); + listCache.set("openschoollist", openSchoolList); + log.info(req.url); + }) + .catch((e) => { + log.error( + "getSchoolsList Error", + e.response ? e.response.status : e.message + ); + }); + } else { + const openSchoolList = await listCache.get("openschoollist"); + res.json(openSchoolList); + } +} async function getSchoolList(req, res) { if (await !listCache.has("schoollist")) { const url = `${config.get("server:instituteAPIURL")}/institute/school`; // Update the URL according to your API endpoint diff --git a/frontend/src/components/DistrictSelect.vue b/frontend/src/components/DistrictSelect.vue index 9e236dcd..cbcf8129 100644 --- a/frontend/src/components/DistrictSelect.vue +++ b/frontend/src/components/DistrictSelect.vue @@ -69,10 +69,6 @@ function downloadDistrictsMailing() { Contacts for All Districts - diff --git a/frontend/src/components/SchoolSelect.vue b/frontend/src/components/SchoolSelect.vue index d71915b3..80a6ce3b 100644 --- a/frontend/src/components/SchoolSelect.vue +++ b/frontend/src/components/SchoolSelect.vue @@ -30,10 +30,6 @@ function goToSchoolSearch() { }) } -function downloadAllSchoolsInfo() { - alert('TODO - Implement all schools info extract download') -} - function downloadAllSchoolsMailing() { alert('TODO - Implement all schools mailing extract download') } @@ -93,9 +89,13 @@ function downloadAllSchoolsMailing() { - All Schools - Info + + All Schools Info From 3259fdf81b5bceceb1446abfa550265c6e02bb73 Mon Sep 17 00:00:00 2001 From: Shaun Lum Date: Mon, 30 Oct 2023 16:57:41 -0700 Subject: [PATCH 2/2] downloads for schools --- backend/src/components/cache.js | 1 + backend/src/components/utils.js | 185 +++++++++++++------- backend/src/routes/download-router.js | 6 +- backend/src/routes/institute-router.js | 25 +++ frontend/src/components/AuthoritySelect.vue | 8 + frontend/src/components/SchoolSelect.vue | 2 +- 6 files changed, 159 insertions(+), 68 deletions(-) diff --git a/backend/src/components/cache.js b/backend/src/components/cache.js index c88473d4..b10b797c 100644 --- a/backend/src/components/cache.js +++ b/backend/src/components/cache.js @@ -1,6 +1,7 @@ const NodeCache = require("node-cache"); const listCache = new NodeCache({ stdTTL: 21600 }); const schoolCache = new NodeCache({ stdTTL: 21600 }); +const codeCache = new NodeCache({ stdTTL: 21600 }); module.exports = { listCache,schoolCache diff --git a/backend/src/components/utils.js b/backend/src/components/utils.js index 3d1cff48..7964d73c 100644 --- a/backend/src/components/utils.js +++ b/backend/src/components/utils.js @@ -21,7 +21,9 @@ const ALLOWED_FILENAMES = new Set([ 'secretary-treasurer', 'executive-admin-assistant', 'exceldistrictcontacts', - 'excelschoolcontacts' + 'publicschoolcontacts', + 'independschoolcontacts', + 'allschoolcontacts' // Add more allowed filepaths as needed ]); const ALLOWED_SCHOOLCATEGORYCODES = new Set([ @@ -99,83 +101,138 @@ function addDistrictLabels(jsonData, districtList) { } return 0; } - function createSchoolCache(schoolData, schoolGrades) { - const propertyOrder = ["mincode", "schoolNumber","mailing_addressLine1","mailing_addressLine2","mailing_postal","mailing_provinceCode","mailing_countryCode"]; + function rearrangeAndRelabelObjectProperties(object, propertyList) { + const reorderedObject = {}; + propertyList.forEach((propertyInfo) => { + const prop = propertyInfo.property; + const label = propertyInfo.label; + reorderedObject[label] = object.hasOwnProperty(prop) ? object[prop] : ""; + }); + return reorderedObject; + } + function createSchoolCache(schoolData, schoolGrades) { // Preload convertedGrades with schoolGrades.schoolGradeCode and set the value to "N" - const convertedGrades = {}; - schoolGrades.forEach((grade) => { - convertedGrades[grade.schoolGradeCode] = "N"; - }); + // Map over each school object return schoolData.map((school) => { - const addressFields = { - mailing: {}, - physical: {}, - }; - - // Loop through the grades and set the value to "Y" for each grade - school.grades.forEach((grade) => { - convertedGrades[grade.schoolGradeCode] = "Y"; - }); + const convertedGrades = {}; + schoolGrades.forEach((grade) => { + convertedGrades[grade.schoolGradeCode] = "N"; + }); - // Extract and format principal contact information if it exists - const principalContact = school.contacts.find((contact) => contact.schoolContactTypeCode === "PRINCIPAL"); - if (principalContact) { - school.firstName = principalContact.firstName; - school.lastName = principalContact.lastName; - school.email = principalContact.email; - school.phoneNumber = principalContact.phoneNumber; - } + const addressFields = { + mailing: {}, + physical: {}, + }; + + // Loop through the grades and set the value to "Y" for each grade + school.grades.forEach((grade) => { + convertedGrades[grade.schoolGradeCode] = "Y"; + }); - // Loop through addresses and update the fields based on addressTypeCode - school.addresses.forEach((address) => { - if (address.addressTypeCode === "MAILING") { - Object.keys(address).forEach((field) => { - // Exclude the specified fields - if (![ - "createUser", - "updateUser", - "createDate", - "updateDate", - "schoolAddressId", - "schoolId", - "addressTypeCode" - ].includes(field)) { - addressFields.mailing[`mailing_${field}`] = address[field]; - } - }); - } else if (address.addressTypeCode === "PHYSICAL") { - Object.keys(address).forEach((field) => { - if (![ - "createUser", - "updateUser", - "createDate", - "updateDate", - "schoolAddressId", - "schoolId", - "addressTypeCode" - ].includes(field)) { - addressFields.mailing[`physical_${field}`] = address[field]; - } - }); + // Extract and format principal contact information if it exists + const principalContact = school.contacts.find((contact) => contact.schoolContactTypeCode === "PRINCIPAL"); + if (principalContact) { + school.firstName = principalContact.firstName; + school.lastName = principalContact.lastName; + school.email = principalContact.email; + school.phoneNumber = principalContact.phoneNumber; + } + + // Loop through addresses and update the fields based on addressTypeCode + school.addresses.forEach((address) => { + if (address.addressTypeCode === "MAILING") { + Object.keys(address).forEach((field) => { + // Exclude the specified fields + if (![ + "createUser", + "updateUser", + "createDate", + "updateDate", + "schoolAddressId", + "schoolId", + "addressTypeCode" + ].includes(field)) { + addressFields.mailing[`mailing_${field}`] = address[field]; + } + }); + } else if (address.addressTypeCode === "PHYSICAL") { + Object.keys(address).forEach((field) => { + if (![ + "createUser", + "updateUser", + "createDate", + "updateDate", + "schoolAddressId", + "schoolId", + "addressTypeCode" + ].includes(field)) { + addressFields.mailing[`physical_${field}`] = address[field]; + } + }); } }); // Concatenate neighborhoodLearningTypeCode into a single string const nlc = school.neighborhoodLearning.map(learning => learning.neighborhoodLearningTypeCode).join(' | '); - // Create a new object with the properties rearranged - const rearrangedSchool = {}; - propertyOrder.forEach((prop) => { - rearrangedSchool[prop] = school[prop]; - }); - // Merge the address fields and nlc into the school object - Object.assign(rearrangedSchool, convertedGrades, addressFields.mailing, addressFields.physical, { nlc }); - - return rearrangedSchool; + Object.assign(school, convertedGrades, addressFields.mailing, addressFields.physical, { nlc }); + + // Remove the original grades property and the updated address object + delete school.grades; + delete school.addresses; + delete school.neighborhoodLearning; + delete school.createUser; + delete school.updateUser; + delete school.updateDate; + delete school.createDate; + delete school.schoolId; + delete school.openedDate; + delete school.closedDate; + delete school.notes; + delete school.schoolMove.createUser; + delete school.schoolMove; + + // Remove the contacts property + delete school.contacts; + const propertyOrder = [ + { property: "districtNumber", label: "District Number" }, + { property: "mincode", label: "School Code" }, + { property: "displayName", label: "School Name" }, + { property: "mailing_addressLine1", label: "Address" }, + { property: "mailing_city", label: "City" }, + { property: "mailing_provinceCode", label: "Province" }, + { property: "mailing_postal", label: "Postal Code" }, + // { property: "principalTitle", label: "Principal Title" }, + { property: "firstName", label: "Principal First Name" }, + { property: "lastName", label: "Principal Last Name" }, + { property: "schoolCategoryCode", label: "Type" }, + // { property: "gradeRange", label: "Grade Range" }, + // { property: "schoolCategory", label: "School Category" }, + // { property: "fundingGroups", label: "Funding Group(s)" }, + { property: "phoneNumber", label: "Phone" }, + // { property: "fax", label: "Fax" }, + { property: "email", label: "Email" }, + { property: "GRADE01", label: "Grade 1 Enrollment" }, + { property: "GRADE02", label: "Grade 2 Enrollment" }, + { property: "GRADE03", label: "Grade 3 Enrollment" }, + { property: "GRADE04", label: "Grade 4 Enrollment" }, + { property: "GRADE05", label: "Grade 5 Enrollment" }, + { property: "GRADE06", label: "Grade 6 Enrollment" }, + { property: "GRADE07", label: "Grade 7 Enrollment" }, + { property: "GRADE08", label: "Grade 8 Enrollment" }, + { property: "GRADE09", label: "Grade 9 Enrollment" }, + { property: "GRADE10", label: "Grade 10 Enrollment" }, + { property: "GRADE11", label: "Grade 11 Enrollment" }, + { property: "GRADE12", label: "Grade 12 Enrollment" } + + + ]; + const schools = rearrangeAndRelabelObjectProperties(school,propertyOrder) + return schools; }); } module.exports = { createList, isSafeFilePath,isAllowedSchoolCategory, addDistrictLabels, districtNumberSort, createSchoolCache }; \ No newline at end of file diff --git a/backend/src/routes/download-router.js b/backend/src/routes/download-router.js index f029cf58..7dc4a14e 100644 --- a/backend/src/routes/download-router.js +++ b/backend/src/routes/download-router.js @@ -47,8 +47,8 @@ async function addDistrictLabels(req, res, next) { let districtList = []; - if (listCache.has("districtList")) { - districtList= listCache.get("districtList"); + if (listCache.has("districtlist")) { + districtList= listCache.get("districtlist"); } else { try { const path = "/api/v1/institute/district/list" @@ -56,7 +56,7 @@ async function addDistrictLabels(req, res, next) { const response = await axios.get(url, { headers: { Authorization: `Bearer ${req.accessToken}` } }); const districts = response.data; - listCache.set("districtList", districts); + listCache.set("districtlist", districts); districtList = districts } catch (error) { diff --git a/backend/src/routes/institute-router.js b/backend/src/routes/institute-router.js index 84f428ae..4ce162fb 100644 --- a/backend/src/routes/institute-router.js +++ b/backend/src/routes/institute-router.js @@ -30,6 +30,7 @@ const openSchoolListOptions = { fields: [ //Batch Routes router.get("/contact-type-codes", checkToken, getContactTypeCodes); +router.get("/grade-codes", checkToken, getGradeCodes); router.get("/offshore-school/list", checkToken, getOffshoreSchoolList); // router.get("/school", checkToken, getOpenSchools); router.get("/school/list", checkToken, getSchoolList); @@ -242,4 +243,28 @@ async function getDistrictContactsAPI(req, res) { log.error("getData Error", e.response ? e.response.status : e.message); }); } + +async function getGradeCodes(req, res) { + if (await !codeCache.has("gradelist")) { + const url = `${config.get("server:instituteAPIURL")}/institute/grade-codes`; // Update the URL according to your API endpoint + axios + .get(url, { headers: { Authorization: `Bearer ${req.accessToken}` } }) + .then((response) => { + //const districtList = response.data; + const gradeCodes = response.data; + codeCache.set("gradelist", gradeList); + res.json(gradeCodes); + log.info(req.url); + }) + .catch((e) => { + log.error( + "getDistrictList Error", + e.response ? e.response.status : e.message + ); + }); + } else { + const gradeList = await codeCache.get("gradelist"); + res.json(gradeList); + } +} module.exports = router; diff --git a/frontend/src/components/AuthoritySelect.vue b/frontend/src/components/AuthoritySelect.vue index 1016584c..f51ef6e0 100644 --- a/frontend/src/components/AuthoritySelect.vue +++ b/frontend/src/components/AuthoritySelect.vue @@ -53,6 +53,14 @@ function downloadAuthorityContacts() { /> + + + All Independent Schools Contacts for All Authorities