import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; import {Sites} from "./sites"; import { Roles } from 'meteor/alanning:roles'; import {parse} from 'csv-parse'; import { ReactiveAggregate } from 'meteor/tunguska:reactive-aggregate'; export const Students = new Mongo.Collection('students'); if (Meteor.isServer) { Students.createIndex({id: 1}, {name: "External ID", unique: true}); // This code only runs on the server Meteor.publish('students', function(siteId) { return Students.find({siteId}); }); Meteor.publish('studentWithAssetAssignments', function(query) { ReactiveAggregate(this, Students, {$lookup: { from: 'assetAssignments', localField: '_id', foreignField: 'assigneeId', as: 'assignments' }}, {}); //Note: The options can use {clientCollection: 'your_name_here'} as the options to change the collection name on the client. }); Meteor.methods({ 'students.getPossibleGrades'() { return Students.rawCollection().distinct('grade', {}); }, /** * Sets a first name alias that can be overridden by the one that is imported. * @param _id The student's database ID. * @param alias The alias to set for the student. */ 'students.setAlias'(_id, alias) { if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { check(_id, String); check(alias, String); Students.update({_id}, !alias || !alias.length() ? {$unset: {alias: true}} : {$set: {alias}}); } }, /** * Assumes that the ID field is a unique ID that never changes for a student. * This must be true in order for duplicate students to be avoided. * Will automatically update a student's data, including the site he/she is associated with. * * 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` * 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];`. * Run the query in Aeries as a `Report`, select TXT, and upload here. * * 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' */ 'students.loadCsv'(csv, type, siteId) { if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { check(csv, String); check(siteId, String); let site = Sites.findOne({_id: siteId}); 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). // Remove headers from the CSV. for(const line of lines) { if (skip > 0) skip--; else if (pageHeader && line === pageHeader) { skip = 2; } else { if(!cleanCsv) cleanCsv = ""; else cleanCsv += '\r\n'; cleanCsv += line; } } //console.log(cleanCsv); // Note: This doesn't work because some values are quoted and contain commas as a value character. // Parse the CSV (now without any headers). // lines = cleanCsv.split(/\r\n/); // for(const line of lines) { // let values = line.split(/\s*,\s*/); // // if(values.length >= 5) { // 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}; // // // console.log(student); // // Update or insert in the db. // console.log("Upserting: " + student); // Students.upsert({id}, {$set: student}); // } // } const bound = Meteor.bindEnvironment((callback) => {callback();}); 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); } //console.log(student); try { Students.upsert({id: student.id}, {$set: student}); } catch(err) { console.error(err); } } console.log(duplicates.length + " records were duplicates:"); console.log(duplicates); } }); }) } else { console.log("Failed to find the site with the ID: " + siteId); } } } }); }