diff --git a/.meteor/packages b/.meteor/packages
index d9eb973..6e283e7 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -6,14 +6,14 @@
meteor-base@1.5.1 # Packages every Meteor app needs to have
mobile-experience@1.1.0 # Packages for a great mobile UX
-mongo@1.16.5 # The database Meteor supports right now
+mongo@1.16.6 # The database Meteor supports right now
jquery # Wrapper package for npm-installed jquery
reactive-var@1.0.12 # Reactive variable for tracker
-standard-minifier-css@1.9.0 # CSS minifier run for production mode
+standard-minifier-css@1.9.2 # CSS minifier run for production mode
standard-minifier-js@2.8.1 # JS minifier run for production mode
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
-ecmascript@0.16.6 # Enable ECMAScript2015+ syntax in app code
+ecmascript@0.16.7 # Enable ECMAScript2015+ syntax in app code
typescript@4.9.4 # Enable TypeScript syntax in .ts and .tsx modules
shell-server@0.5.0 # Server-side component of the `meteor shell` command
@@ -27,3 +27,4 @@ google-config-ui@1.0.3 # Adds the UI for logging in via Google
alanning:roles # Adds roles to the user
msavin:mongol # Free version of MeteorToys - Provides access to the client side MongoDB for debugging. (Ctrl-M to activate :: https://atmospherejs.com/msavin/mongol)
+session
diff --git a/.meteor/release b/.meteor/release
index 31dd9a2..e8cfc7e 100644
--- a/.meteor/release
+++ b/.meteor/release
@@ -1 +1 @@
-METEOR@2.11.0
+METEOR@2.12
diff --git a/.meteor/versions b/.meteor/versions
index 5bfabae..45a310f 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -1,4 +1,4 @@
-accounts-base@2.2.7
+accounts-base@2.2.8
accounts-google@1.4.0
accounts-oauth@1.4.2
accounts-password@2.3.4
@@ -7,30 +7,30 @@ accounts-ui-unstyled@1.7.0
alanning:roles@3.4.0
allow-deny@1.1.1
autoupdate@1.8.0
-babel-compiler@7.10.3
+babel-compiler@7.10.4
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
-blaze@2.6.1
+blaze@2.6.2
blaze-tools@1.1.3
boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
-callback-hook@1.5.0
+callback-hook@1.5.1
check@1.3.2
ddp@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
-ddp-rate-limiter@1.1.1
-ddp-server@2.6.0
+ddp-rate-limiter@1.2.0
+ddp-server@2.6.1
diff-sequence@1.1.2
-dynamic-import@0.7.2
-ecmascript@0.16.6
-ecmascript-runtime@0.8.0
+dynamic-import@0.7.3
+ecmascript@0.16.7
+ecmascript-runtime@0.8.1
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.3
-email@2.2.4
+email@2.2.5
es5-shim@4.8.0
fetch@0.1.3
geojson-utils@1.0.11
@@ -46,31 +46,31 @@ launch-screen@1.3.0
less@4.0.0
localstorage@1.2.0
logging@1.3.2
-meteor@1.11.1
+meteor@1.11.2
meteor-base@1.5.1
meteortoys:toykit@10.0.0
-minifier-css@1.6.2
+minifier-css@1.6.4
minifier-js@2.7.5
-minimongo@1.9.2
+minimongo@1.9.3
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.9
modules@0.19.0
modules-runtime@0.13.1
-mongo@1.16.5
+mongo@1.16.6
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
msavin:mongol@10.0.1
-npm-mongo@4.14.0
+npm-mongo@4.16.0
oauth@2.2.0
oauth2@1.3.2
-observe-sequence@1.0.20
+observe-sequence@1.0.21
ordered-dict@1.1.0
promise@0.12.2
random@1.2.1
-rate-limit@1.0.9
-react-fast-refresh@0.2.6
+rate-limit@1.1.1
+react-fast-refresh@0.2.7
react-meteor-data@2.7.2
reactive-dict@1.3.1
reactive-var@1.0.12
@@ -81,19 +81,19 @@ service-configuration@1.3.1
session@1.2.1
sha@1.0.9
shell-server@0.5.0
-socket-stream-client@0.5.0
+socket-stream-client@0.5.1
spacebars@1.3.0
spacebars-compiler@1.3.1
-standard-minifier-css@1.9.0
+standard-minifier-css@1.9.2
standard-minifier-js@2.8.1
static-html@1.3.2
templating@1.4.2
templating-compiler@1.4.1
-templating-runtime@1.6.1
+templating-runtime@1.6.3
templating-tools@1.2.2
-tracker@1.3.1
+tracker@1.3.2
typescript@4.9.4
-underscore@1.0.12
+underscore@1.0.13
url@1.3.2
-webapp@1.13.4
+webapp@1.13.5
webapp-hashing@1.1.1
diff --git a/imports/api/assets.js b/imports/api/assets.js
index 5e1a4fa..f388846 100644
--- a/imports/api/assets.js
+++ b/imports/api/assets.js
@@ -10,6 +10,7 @@ import {AssetAssignmentHistory} from "/imports/api/asset-assignment-history";
export const Assets = new Mongo.Collection('assets');
export const conditions = ['New','Like New','Good','Okay','Damaged', 'Missing', 'Decommissioned']
+export const functionalConditions = ['New','Like New','Good','Okay']
/*
const AssetsSchema = new SimpleSchema({
@@ -135,6 +136,24 @@ Meteor.methods({
}
else throw new Meteor.Error("User Permission Error");
},
+ 'assets.updateCondition'(_id, condition, conditionDetails) {
+ console.log("updating condtition: " + condition + " / " + conditionDetails)
+ check(_id, String)
+ check(condition, String)
+ if(conditionDetails) check(conditionDetails, String)
+
+ if(!conditions.includes(condition)) {
+ //Should never happen.
+ console.error("Invalid condition option in assets.update(..)");
+ throw new Meteor.Error("Invalid condition option.");
+ }
+
+ if(Roles.userIsInRole(Meteor.userId(), "laptop-management", {anyScope:true})) {
+ console.log("updating .... ")
+ Assets.update({_id}, {$set: {condition, conditionDetails}});
+ }
+ else throw new Meteor.Error("User Permission Error");
+ },
'assets.remove'(_id) {
check(_id, String);
diff --git a/imports/api/sites.js b/imports/api/sites.js
index efe2f49..1aee66b 100644
--- a/imports/api/sites.js
+++ b/imports/api/sites.js
@@ -13,14 +13,14 @@ if (Meteor.isServer) {
});
}
Meteor.methods({
- 'sites.update'(_id, name) {
+ 'sites.update'(_id, name, externalId) {
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
- Sites.update({_id}, {$set: {name}});
+ Sites.update({_id}, {$set: {name, externalId}});
}
},
- 'sites.add'(name) {
+ 'sites.add'(name, externalId) {
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
- Sites.insert({name});
+ Sites.insert({name, externalId});
}
},
'sites.remove'(_id) {
diff --git a/imports/api/students.js b/imports/api/students.js
index 6fdc83c..ace50a7 100644
--- a/imports/api/students.js
+++ b/imports/api/students.js
@@ -17,19 +17,23 @@ if (Meteor.isServer) {
});
Meteor.methods({
- 'students.add'(id, firstName, lastName, email, siteId, grade) {
+ 'students.add'(id, firstName, firstNameAlias, lastName, email, siteId, grade, active) {
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
- Students.insert({id, firstName, lastName, email, siteId, grade});
+ Students.insert({id, firstName, firstNameAlias, lastName, email, siteId, grade, active, activeChangeTimestamp: active ? "" : new Date()});
}
},
- 'students.update'(_id, id, firstName, lastName, email, siteId, grade) {
+ 'students.update'(_id, id, firstName, firstNameAlias, lastName, email, siteId, grade, active) {
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
- Students.update({_id}, {$set: {id, firstName, lastName, email, siteId, grade}});
+ Students.update({_id}, {$set: {id, firstName, firstNameAlias, lastName, email, siteId, grade, active, activeChangeTimestamp: active ? "" : new Date()}});
}
},
'students.remove'(_id) {
+ // Does not actually remove the student (not currently possible. Does set the student to not-active.
+ // If we want to remove students we should allow it for non-active students if there are no assets assigned.
+ // We may want to do this automatically, perhaps for students that have been non-active for a long period of time.
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
- //TODO: Need to first verify there are no checked out assets to the staff member.
+ // Set the student as non-active and set the timestamp for the change (so we know how long they have been inactive for - so we can potentially automatically remove them later.
+ Students.update({_id}, {$set: {active: false, activeChangeTimestamp: new Date()}})
}
},
'students.getPossibleGrades'() {
@@ -56,95 +60,183 @@ if (Meteor.isServer) {
* Expects the CSV string to contain comma delimited data in the form:
* email, student ID, first name, last name, grade, first name alias, last name alias
*
- * The query in Aeries is: `LIST STU ID SEM FN LN NG FNA`.
- * A more complete Aeries query: `LIST STU STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.NG STU.SEM IF STU.NG >= 7 AND NG <= 12 AND STU.NS = 5`
+ * The query in Aeries is: `LIST STU NS ID SEM FN LN NG FNA IF NG <= 12`.
+ * A more complete Aeries query (for grades 7-12 in school 5): `LIST STU STU.NS STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.NG STU.SEM IF STU.NG >= 7 AND NG <= 12 AND STU.NS = 5`
* Note that FNA (First Name Alias) is optional.
* Note that you might want to include a school ID in the IF if you have multiple schools in the district.
- * The query in SQL is: `SELECT [STU].[ID] AS [Student ID], [STU].[SEM] AS [StuEmail], STU.FN AS [First Name], STU.LN AS [Last Name], [STU].[GR] AS [Grade], [STU].[FNA] AS [First Name Alias], [STU].[LNA] AS [Last Name Alias] FROM (SELECT [STU].* FROM STU WHERE [STU].DEL = 0) STU WHERE ( [STU].SC = 5) ORDER BY [STU].[LN], [STU].[FN];`.
+ * The query in SQL is: `SELECT [STU].[NS] AS [Next Schl], [STU].[ID] AS [Student ID], [STU].[SEM] AS [StuEmail], STU.FN AS [First Name], STU.LN AS [Last Name], [STU].[GR] AS [Grade], [STU].[FNA] AS [First Name Alias], [STU].[LNA] AS [Last Name Alias] FROM (SELECT [STU].* FROM STU WHERE [STU].DEL = 0) STU WHERE ( [STU].SC = 5) ORDER BY [STU].[LN], [STU].[FN];`.
* Run the query in Aeries as a `Report`, select TXT, and upload here.
+ *
+ * Note: The headers for the CSV are not important and will be ignored. The order of the data is very important.
*
* Aeries adds a header per 'page' of data (I think 35 entries per page).
* Example:
* Anderson Valley Jr/Sr High School,6/11/2022
* 2021-2022,Page 1
* Student ID, Email, First Name,Last Name,Grade,(opt) First Name Alias
- * @type: Currently only supports 'CSV' or 'Aeries Text Report'
+ * @type: Currently only supports 'csv' or 'aeries-txt'
+ *
+ * TODO: We are assuming that we are importing all active students from the external system. Any other assumption would require too much in the way of GUI
+ * TODO: Import should have a site id column that is the external site id.
+ * TODO: Each imported student should be attached to the correct site
+ * TODO: Any students not imported should be marked as deactivated
*/
- 'students.loadCsv'(csv, type, siteId) {
- if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
- check(csv, String);
- check(siteId, String);
+ 'students.loadCsv'(csv, type, test) {
+ try {
+ if (Roles.userIsInRole(Meteor.userId(), "admin", {anyScope: true})) {
+ check(csv, String)
- let site = Sites.findOne({_id: siteId});
+ let sites = Sites.find().fetch()
+ let sitesByExternalId = {}
- if(site) {
- let cleanCsv;
- let lines = csv.split(/\r?\n/);
- let pageHeader = type === 'Aeries Text Report' ? lines[0] : null; // Skip the repeating header lines for an Aeries text report.
- let skip = type === 'CSV' ? 1 : 0; // Skip the first line of a CSV file (headers).
+ // Map all sites by external ID so we can quickly find the site for each imported student.
+ for (let site of sites)
+ if (site.externalId)
+ sitesByExternalId[site.externalId] = site
+
+ //Note: Only include active students since we don't want to repeatedly make students non-active (resetting the timestamp).
+ let existingStudents = Students.find({active: true}).fetch()
+ let existingStudentIds = new Set()
+
+ // Collect all pre-existing student ID's. Will remove them as we import, and use the remaining set to de-activate the students no longer in the district.
+ for (let student of existingStudents)
+ existingStudentIds.add(student.id)
+
+ let cleanCsv
+ let lines = csv.split(/\r?\n/)
+ let pageHeader = type === 'aeries-txt' ? lines[0] : null // Skip the repeating header lines for an Aeries text report.
+ let skip = type === 'csv' ? 1 : 0 // Skip the first line of a CSV file (headers).
// Remove headers from the CSV.
- for(const line of lines) {
- if (skip > 0) skip--;
+ for (const line of lines) {
+ if (skip > 0) skip--
else if (pageHeader && line === pageHeader) {
- skip = 2;
+ skip = 2
} else {
- if(!cleanCsv) cleanCsv = "";
- else cleanCsv += '\r\n';
- cleanCsv += line;
+ if (!cleanCsv) cleanCsv = ""
+ else cleanCsv += '\r\n'
+ cleanCsv += line
}
}
- const bound = Meteor.bindEnvironment((callback) => {callback();});
+ const bound = Meteor.bindEnvironment((callback) => {
+ callback();
+ })
- parse(cleanCsv, {}, function(err, records) {
+ parse(cleanCsv, {}, function (err, records) {
bound(() => {
- if(err) console.error(err);
- else {
- let foundIds = new Set();
- let duplicates = [];
- console.log("Found " + records.length + " records.");
-
- for(const values of records) {
- let id = values[0];
- let email = values[1];
- let firstName = values[2];
- let lastName = values[3];
- let grade = parseInt(values[4], 10);
- let firstNameAlias = "";
- let active = true;
-
- if(values.length > 5) firstNameAlias = values[5];
-
- let student = {siteId, email, id, firstName, lastName, grade, firstNameAlias, active};
-
- // Track the student ID's and record duplicates. This is used to ensure our counts are accurate later.
- if(foundIds.has(student.id)) {
- duplicates.push(student.id);
- }
- else {
- foundIds.add(student.id);
- }
-
- try {
- Students.upsert({id: student.id}, {$set: student});
- }
- catch(err) {
- console.error(err);
- }
- }
-
- console.log(duplicates.length + " records were duplicates:");
- console.log(duplicates);
- }
- });
+ readCsv(err, records, sitesByExternalId, existingStudentIds, test)
+ })
})
}
- else {
- console.log("Failed to find the site with the ID: " + siteId);
- }
+ } catch(err) {
+ console.log(err)
}
}
- });
+ })
+
+ /**
+ * Reads the CSV file containing Student data and updates and adds students to the system. Students not in the CSV are marked as non-active.
+ * @param err
+ * @param records
+ * @param sitesByExternalId
+ * @param existingStudentIds
+ * @param test
+ * @returns {string}
+ */
+ const readCsv = (err, records, sitesByExternalId, existingStudentIds, test) => {
+ let output = ""
+
+ if (err) console.error(err)
+ else {
+ let foundIds = new Set()
+ let duplicates = []
+ let count = 0
+ let nonActiveCount = 0
+
+ if (test)
+ output += "Found " + records.length + " records.\r\n"
+
+ try {
+ for(const values of records) {
+ let nextSchool = values[0]
+ let siteId = sitesByExternalId[nextSchool] ? sitesByExternalId[nextSchool]._id : null
+ let id = values[1]
+ let email = values[2]
+ let firstName = values[3]
+ let lastName = values[4]
+ let grade = parseInt(values[5], 10)
+ let firstNameAlias = ""
+ let active = true
+
+ if (values.length > 6) firstNameAlias = values[6];
+
+ // Ignore students at a site not in the system.
+ if(siteId) {
+ let student = { siteId, email, id, firstName, lastName, grade, firstNameAlias, active, activeChangeTimestamp: ""}
+
+ // Track the student ID's and record duplicates. This is used to ensure our counts are accurate later.
+ // Note: We should never have duplicates in a perfect system, but in reality we do seem to end up with some duplicates in the SIS system's data.
+ // There can be perfectly understandable reasons for this, so we will ignore them here since it shouldn't affect us.
+ if (foundIds.has(student.id)) {
+ duplicates.push(student.id)
+ } else {
+ foundIds.add(student.id)
+ }
+
+ count++
+
+ if (!test) {
+ try {
+ existingStudentIds.delete(student.id)
+ Students.upsert({id: student.id}, {$set: student})
+ } catch (err) {
+ console.log("Error while calling Students.upsert(..)")
+ console.error(err)
+ }
+ } else {
+ if (existingStudentIds.has(student.id)) {
+ existingStudentIds.delete(student.id)
+ output += "Updating existing student: " + student + "\r\n"
+ } else output += "Adding student: " + student + "\r\n"
+ }
+ }
+ }
+ } catch(err) {
+ console.log("Caught exception (while processing students imported via CSV): ")
+ console.log(err)
+ }
+
+ // Change active status for all remaining students in the set (ones who were not in the import).
+ for (let studentId of existingStudentIds) {
+ nonActiveCount++
+
+ if (test) {
+ output += "Changing active status for student: " + Students.findOne({id: studentId}) + "\r\n"
+ } else {
+ try {
+ Students.update({id: studentId}, {
+ $set: {
+ active: false,
+ activeChangeTimestamp: new Date()
+ }
+ })
+ } catch (err) {
+ console.log("Student ID: " + studentId)
+ console.log("Error updating Student to be non-active:")
+ console.log(err)
+ }
+ }
+ }
+
+ console.log(duplicates.length + " records were duplicates:")
+ console.log(duplicates)
+ console.log("")
+ console.log("Added or updated " + count + " students.")
+ console.log("Update " + nonActiveCount + " students to non-active status.")
+ }
+
+ return output
+ }
}
\ No newline at end of file
diff --git a/imports/ui/App.jsx b/imports/ui/App.jsx
index 5d7b3b5..5adc1c3 100644
--- a/imports/ui/App.jsx
+++ b/imports/ui/App.jsx
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import {Roles} from 'meteor/alanning:roles';
-import React, { useState } from 'react';
+import React, {useEffect, useState} from 'react';
import {createTheme, ThemeProvider} from '@mui/material/styles'
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
diff --git a/imports/ui/pages/Admin/Sites.jsx b/imports/ui/pages/Admin/Sites.jsx
index ae60a66..9dbbaf0 100644
--- a/imports/ui/pages/Admin/Sites.jsx
+++ b/imports/ui/pages/Admin/Sites.jsx
@@ -28,14 +28,15 @@ const cssButtonContainer = {
const SiteEditor = ({value, close}) => {
const [name, setName] = useState(value.name || "")
+ const [externalId, setExternalId] = useState(value.externalId || "")
const applyChanges = () => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
- Meteor.call("sites.update", value._id, name);
+ Meteor.call("sites.update", value._id, name, externalId);
else
- Meteor.call("sites.add", name);
+ Meteor.call("sites.add", name, externalId);
}
const rejectChanges = () => {
close()
@@ -45,6 +46,7 @@ const SiteEditor = ({value, close}) => {