diff --git a/.meteor/packages b/.meteor/packages
index 8990821..6e283e7 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -6,24 +6,25 @@
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.15.0 # 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.11 # Reactive variable for tracker
+reactive-var@1.0.12 # Reactive variable for tracker
-standard-minifier-css@1.8.1 # CSS minifier run for production mode
-standard-minifier-js@2.8.0 # JS 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.2 # Enable ECMAScript2015+ syntax in app code
-typescript # Enable TypeScript syntax in .ts and .tsx modules
+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
static-html@1.3.2 # Define static page content in .html files
react-meteor-data # React higher-order component for reactively tracking Meteor data
accounts-ui@1.4.2
-accounts-password@2.3.1
+accounts-password@2.3.4
accounts-google@1.4.0
-service-configuration@1.3.0
+service-configuration@1.3.1
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 66dd7b6..e8cfc7e 100644
--- a/.meteor/release
+++ b/.meteor/release
@@ -1 +1 @@
-METEOR@2.7.3
+METEOR@2.12
diff --git a/.meteor/versions b/.meteor/versions
index 0f7c141..45a310f 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -1,41 +1,41 @@
-accounts-base@2.2.4
+accounts-base@2.2.8
accounts-google@1.4.0
-accounts-oauth@1.4.1
-accounts-password@2.3.1
+accounts-oauth@1.4.2
+accounts-password@2.3.4
accounts-ui@1.4.2
accounts-ui-unstyled@1.7.0
alanning:roles@3.4.0
allow-deny@1.1.1
autoupdate@1.8.0
-babel-compiler@7.9.2
+babel-compiler@7.10.4
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
-blaze@2.5.0
+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.4.0
-check@1.3.1
-ddp@1.4.0
-ddp-client@2.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.0
-ddp-server@2.5.0
-diff-sequence@1.1.1
-dynamic-import@0.7.2
-ecmascript@0.16.2
-ecmascript-runtime@0.8.0
+ddp-rate-limiter@1.2.0
+ddp-server@2.6.1
+diff-sequence@1.1.2
+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.2
-email@2.2.1
+ejson@1.1.3
+email@2.2.5
es5-shim@4.8.0
-fetch@0.1.1
-geojson-utils@1.0.10
+fetch@0.1.3
+geojson-utils@1.0.11
google-config-ui@1.0.3
-google-oauth@1.4.2
+google-oauth@1.4.3
hot-code-push@1.0.4
html-tools@1.1.3
htmljs@1.1.1
@@ -43,57 +43,57 @@ id-map@1.1.1
inter-process-messaging@0.1.1
jquery@3.0.0
launch-screen@1.3.0
-less@3.0.2
+less@4.0.0
localstorage@1.2.0
-logging@1.3.1
-meteor@1.10.0
+logging@1.3.2
+meteor@1.11.2
meteor-base@1.5.1
meteortoys:toykit@10.0.0
-minifier-css@1.6.1
+minifier-css@1.6.4
minifier-js@2.7.5
-minimongo@1.8.0
+minimongo@1.9.3
mobile-experience@1.1.0
mobile-status-bar@1.1.0
-modern-browsers@0.1.8
-modules@0.18.0
-modules-runtime@0.13.0
-mongo@1.15.0
+modern-browsers@0.1.9
+modules@0.19.0
+modules-runtime@0.13.1
+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.3.1
-oauth@2.1.2
-oauth2@1.3.1
-observe-sequence@1.0.20
+npm-mongo@4.16.0
+oauth@2.2.0
+oauth2@1.3.2
+observe-sequence@1.0.21
ordered-dict@1.1.0
-promise@0.12.0
-random@1.2.0
-rate-limit@1.0.9
-react-fast-refresh@0.2.3
-react-meteor-data@2.5.1
-reactive-dict@1.3.0
-reactive-var@1.0.11
+promise@0.12.2
+random@1.2.1
+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
reload@1.3.1
retry@1.1.0
routepolicy@1.1.1
-service-configuration@1.3.0
-session@1.2.0
+service-configuration@1.3.1
+session@1.2.1
sha@1.0.9
shell-server@0.5.0
-socket-stream-client@0.5.0
-spacebars@1.2.0
+socket-stream-client@0.5.1
+spacebars@1.3.0
spacebars-compiler@1.3.1
-standard-minifier-css@1.8.2
+standard-minifier-css@1.9.2
standard-minifier-js@2.8.1
static-html@1.3.2
-templating@1.4.1
+templating@1.4.2
templating-compiler@1.4.1
-templating-runtime@1.5.0
+templating-runtime@1.6.3
templating-tools@1.2.2
-tracker@1.2.0
-typescript@4.5.4
-underscore@1.0.10
+tracker@1.3.2
+typescript@4.9.4
+underscore@1.0.13
url@1.3.2
-webapp@1.13.1
-webapp-hashing@1.1.0
+webapp@1.13.5
+webapp-hashing@1.1.1
diff --git a/imports/api/asset-assignment-history.js b/imports/api/asset-assignment-history.js
index 3a7af45..1c6f5ef 100644
--- a/imports/api/asset-assignment-history.js
+++ b/imports/api/asset-assignment-history.js
@@ -34,8 +34,16 @@ if (Meteor.isServer) {
* @returns {any} Array of Asset Assignment History objects.
*/
'AssetAssignmentHistory.get'(params) {
+ let result
+
if(Roles.userIsInRole(Meteor.userId(), "laptop-management", {anyScope:true})) {
- let query = {};
+ let query = {}
+ let person
+ let asset
+ let assetType
+
+ // console.log("AssetAssignmentHistory: ")
+ // console.log(params)
if(params.studentId) check(params.studentId, String)
if(params.staffId) check(params.staffId, String)
@@ -56,81 +64,52 @@ if (Meteor.isServer) {
}
}
- if(params.serial) query.serial = params.serial;
- else if(params.assetId) query.assetId = params.assetId;
- else if(params.deviceId) query.deviceId = params.deviceId;
- else if(params.studentId) {
- query.assigneeId = params.studentId
- query.assigneeType = "Student"
- }
- else if(params.staffId) {
- query.assigneeId = params.staffId
- query.assigneeType = "Staff"
+ if(params.serial || params.assetId || params.deviceId) {
+ if(params.serial) query.serial = params.serial;
+ else if(params.assetId) query.assetId = params.assetId;
+ else if(params.deviceId) query.deviceId = params.deviceId;
+
+ asset = Assets.findOne(query)
+ if(asset) assetType = AssetTypes.findOne({_id: asset.assetTypeId})
}
else {
- query = undefined;
- }
-
- if(query) {
- //Sort by the last time the record was updated from most to least recent.
- let result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch();
- let assets = [];
-
- // Get the current assignment for the device or person.
- if(query.assetId || query.deviceId || query.serial) {
- let asset = Assets.findOne(query)
-
- if(asset) assets = [asset]
+ if(params.studentId) {
+ query.assigneeId = params.studentId
+ query.assigneeType = "Student"
+ }
+ else if(params.staffId) {
+ query.assigneeId = params.staffId
+ query.assigneeType = "Staff"
}
else {
- // Find the assets assigned to the person.
- assets = Assets.find({assigneeId: params.studentId ? params.studentId : params.staffId}).fetch()
+ query = undefined;
}
+
+ person = query.assigneeType === "Student" ? Students.findOne({id: query.assigneeId}) : Staff.findOne({id: query.assigneeId})
+ }
+
+ if(query) {
+ //Sort by the last time the record was updated from most to least recent.
+ result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch();
- // Prepend a partial assignment history record to the list. We want to show active assignments in the results.
- for(let asset of assets) {
- if(asset && asset.assigneeId) {
- let assetType = AssetTypes.findOne(asset.assetTypeId)
- let current = {
- _id: 0,
- assetKey: asset._id,
- assetId: asset.assetId,
- serial: asset.serial,
- assetTypeName: assetType.name,
- assigneeType: asset.assigneeType,
- assigneeId: asset.assigneeId,
- startDate: asset.assignmentDate,
- startCondition: asset.condition,
- startConditionDetails: asset.conditionDetails
- }
-
- result = [current, ...result]
- }
- }
-
- //Add some additional data to the records.
+ //Expand the assignee, asset, and asset type data.
for(let next of result) {
- // console.log(next)
- if(next.assetKey) {
- next.asset = Assets.findOne({_id: next.assetKey})
+ if(person) next.assignee = person
+ else next.assignee = next.assigneeType === "Student" ? Students.findOne({_id: next.assigneeId}) : Staff.findOne({_id: next.assigneeId})
+
+ if(asset) {
+ next.asset = asset
+ next.assetType = assetType
}
- else if(next.assetId) {
- next.asset = Assets.findOne({assetId: next.assetId});
- }
-
- if(next.asset) {
- next.assetType = AssetTypes.findOne({_id: next.asset.assetTypeId})
- }
-
- if(next.assigneeId) {
- next.assignee = next.asset.assigneeType === "Student" ? Students.findOne({_id: next.assigneeId}) : Staff.findOne({_id: next.assigneeId})
+ else {
+ next.asset = Assets.findOne({assetId: next.assetId})
+ if(next.asset) next.assetType = AssetTypes.findOne({_id: next.asset.assetTypeId})
}
}
-
- return result;
- } else return null;
+ }
}
- else return null;
+
+ return result
}
});
}
\ No newline at end of file
diff --git a/imports/api/assets.js b/imports/api/assets.js
index 5e1a4fa..35f7e1e 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({
@@ -122,6 +123,7 @@ Meteor.methods({
if(serial) check(serial, String);
check(condition, String);
if(conditionDetails) check(conditionDetails, String);
+ const existing = Assets.findOne({_id})
if(!conditions.includes(condition)) {
//Should never happen.
@@ -132,6 +134,30 @@ Meteor.methods({
if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) {
//TODO: Need to first verify there are no checked out assets to the staff member.
Assets.update({_id}, {$set: {assetTypeId, assetId, serial, condition, conditionDetails}});
+
+ if(assetId !== existing.assetId) {
+ //When changing the asset id we also need to update the other locations in the data where that ID exists.
+ // assetAssignmentHistory.assetId
+ AssetAssignmentHistory.updateMany({assetId: existing.assetId}, {$set: {assetId}})
+ }
+ }
+ 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");
},
@@ -198,7 +224,7 @@ Meteor.methods({
throw new Meteor.Error("Asset is already assigned.", "Cannot assign an asset that has already been assigned.");
}
else {
- Assets.update({assetId}, {$set: {assigneeType, assigneeId, assignmentDate: date, condition, conditionDetails}});
+ Assets.update({assetId}, {$set: {assigneeType, assigneeId, assignmentDate: date, condition, conditionDetails, assignedBy: Meteor.userId()}});
}
}
else {
@@ -238,11 +264,11 @@ Meteor.methods({
let assetType = AssetTypes.findOne({_id: asset.assetTypeId});
try {
- AssetAssignmentHistory.insert({assetKey: asset._id, assetId, serial: asset.serial, assetTypeName: (assetType ? assetType.name : "UNK"), assigneeType: asset.assigneeType, assigneeId: asset.assigneeId, startDate: asset.assignmentDate, endDate: date, startCondition: asset.condition, endCondition: condition, startConditionDetails: asset.conditionDetails, endConditionDetails: conditionDetails, comment});
+ AssetAssignmentHistory.insert({assetKey: asset._id, assetId, serial: asset.serial, assetTypeName: (assetType ? assetType.name : "UNK"), assigneeType: asset.assigneeType, assigneeId: asset.assigneeId, startDate: asset.assignmentDate, endDate: date, startCondition: asset.condition, endCondition: condition, startConditionDetails: asset.conditionDetails, endConditionDetails: conditionDetails, comment, unassignedBy: Meteor.userId(), assignedBy: asset.assignedBy});
} catch (e) {
console.error(e);
}
- Assets.update({assetId}, {$unset: {assigneeType: "", assigneeId: "", assignmentDate: ""}, $set: {condition, conditionDetails}});
+ Assets.update({assetId}, {$unset: {assigneeType: "", assigneeId: "", assignmentDate: "", assignedBy: ""}, $set: {condition, conditionDetails}});
}
else {
console.error("Could not find the asset: " + assetId);
diff --git a/imports/api/data-collection.js b/imports/api/data-collection.js
index 04031ce..bb536b7 100644
--- a/imports/api/data-collection.js
+++ b/imports/api/data-collection.js
@@ -72,7 +72,7 @@ if (Meteor.isServer) {
else if(params.studentId) {
const student = Students.findOne({_id: params.studentId})
- console.log(student)
+ // console.log(student)
if(student) query.email = student.email;
else query = undefined
}
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/AssetTypes.jsx b/imports/ui/pages/Admin/AssetTypes.jsx
index 267e980..6d34e7a 100644
--- a/imports/ui/pages/Admin/AssetTypes.jsx
+++ b/imports/ui/pages/Admin/AssetTypes.jsx
@@ -42,9 +42,9 @@ const AssetTypeEditor = ({value, close}) => {
close()
//TODO Should invert this and only close if there was success on the server.
if(value._id)
- Meteor.call("assetType.update", value._id, name, description, year);
+ Meteor.call("assetTypes.update", value._id, name, description, year);
else
- Meteor.call("assetType.add", name, description, year);
+ Meteor.call("assetTypes.add", name, description, year);
}
const rejectChanges = () => {
close()
@@ -97,7 +97,7 @@ export default () => {
maxHeight: '40rem',
keyHandler: (e, selected) => {
if(selected && selected._id && e.key === "Delete") {
- Meteor.call("assetType.remove", selected._id);
+ Meteor.call("assetTypes.remove", selected._id);
}
}
}
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}) => {