From 4560d7203d1ed4b46006d7225dae637cca840dd2 Mon Sep 17 00:00:00 2001 From: Wynne Crisman Date: Tue, 2 Aug 2022 12:02:56 -0700 Subject: [PATCH] Finished the first cut of adding asset assignments; Added a page to display asset assignments (need to allow removing them). --- imports/api/asset-assignments.js | 29 ++- imports/api/students.js | 277 ++++++++++++++------------- imports/ui/Admin/AssetTypes.svelte | 7 - imports/ui/Admin/Students.svelte | 4 +- imports/ui/Assets.svelte | 4 + imports/ui/Assets/Assign.svelte | 22 ++- imports/ui/Assets/Assignments.svelte | 133 +++++++++++++ 7 files changed, 316 insertions(+), 160 deletions(-) create mode 100644 imports/ui/Assets/Assignments.svelte diff --git a/imports/api/asset-assignments.js b/imports/api/asset-assignments.js index 15e20a3..9178e94 100644 --- a/imports/api/asset-assignments.js +++ b/imports/api/asset-assignments.js @@ -23,12 +23,12 @@ const AssetAssignmentsSchema = new SimpleSchema({ label: "Assignee ID", optional: false, }, - assigneeType: { + assigneeType: { // 0: Student, 1: Staff type: SimpleSchema.Integer, label: "Assignee Type", optional: false, - min: 1, - max: 2, + min: 0, + max: 1, exclusiveMin: false, exclusiveMax: false, }, @@ -49,15 +49,26 @@ if (Meteor.isServer) { }); } Meteor.methods({ - 'assetAssignments.add'(assetId) { - // check(assetTypeId, String); - // check(assetId, String); + /** + * Assigns the asset to the assignee. The assignee should either be a Student or Staff member. + * @param assetId The Mongo ID of the asset (asset._id). + * @param assigneeType One of: 'Student', 'Staff' + * @param assigneeId The Mongo ID of the Student or Staff (person._id). + */ + 'AssetAssignments.add'(assetId, assigneeType, assigneeId) { + check(assigneeId, String); + check(assigneeType, String); + check(assetId, String); - if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { - // Assets.insert({assetTypeId, assetId}); + if(assigneeType !== 'Student' || assigneeType !== 'Staff') { + // Should never happen. + console.error("Error: Received incorrect assignee type in adding an assignment."); + } + else if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { + AssetAssignments.insert({assetId, assigneeType: assigneeType === "Student" ? 0 : 1, assigneeId}); } }, - 'assetAssignments.remove'(_id) { + 'AssetAssignments.remove'(_id) { check(_id, String); if(Roles.userIsInRole(Meteor.userId(), "admin", {anyScope:true})) { diff --git a/imports/api/students.js b/imports/api/students.js index e52628d..b5944ec 100644 --- a/imports/api/students.js +++ b/imports/api/students.js @@ -14,149 +14,150 @@ if (Meteor.isServer) { Meteor.publish('students', function(siteId) { return Students.find({siteId}); }); -} -Meteor.methods({ - async '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); + 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); + 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}); + 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). + 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); + // 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; } - }); - }) - } - else { - console.log("Failed to find the site with the ID: " + siteId); + } + + //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); + } } } - } -}); + }); + +} \ No newline at end of file diff --git a/imports/ui/Admin/AssetTypes.svelte b/imports/ui/Admin/AssetTypes.svelte index b39006b..6fe3fa6 100644 --- a/imports/ui/Admin/AssetTypes.svelte +++ b/imports/ui/Admin/AssetTypes.svelte @@ -13,13 +13,6 @@ const assetTypesColumns = [ { - key: "_id", - title: "ID", - value: v => v._id, - minWidth: 20, - weight: 1, - cls: "id", - }, { key: "name", title: "Name", value: v => v.name, diff --git a/imports/ui/Admin/Students.svelte b/imports/ui/Admin/Students.svelte index 9d24721..5b5c970 100644 --- a/imports/ui/Admin/Students.svelte +++ b/imports/ui/Admin/Students.svelte @@ -176,9 +176,9 @@

Aeries

For the Aeries system, log into your Aeries web interface and navigate to the query page. Enter the following query:

Pre-Rollover

-
LIST STU STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.ID STU.NG STU.SEM IF STU.NG >= 7 AND NG <= 12 AND STU.NS = 5
+
LIST STU STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.ID STU.NG STU.SEM IF STU.NG >= 7 AND STU.NG <= 12 AND STU.NS = 5

Post-Rollover

-
LIST STU STU.ID STU.SEM STU.FN STU.LN STU.NG BY STU.ID STU.NG STU.SEM IF STU.NG >= 7 AND GR <= 12 AND STU.SC = 5
+
LIST STU STU.ID STU.SEM STU.FN STU.LN STU.GR BY STU.ID STU.GR STU.SEM IF STU.GR >= 7 AND STU.GR <= 12 AND STU.SC = 5

Run the query and validate that all students have an email address and a student ID. You likely also want to check that the student `next grade (NG)` field is set correctly for the students (pre-rollover).

You have two options for export. You can:

    diff --git a/imports/ui/Assets.svelte b/imports/ui/Assets.svelte index bcf8755..5a3cc69 100644 --- a/imports/ui/Assets.svelte +++ b/imports/ui/Assets.svelte @@ -7,6 +7,7 @@ import AssetDataEntry from "/imports/ui/Assets/AssetDataEntry.svelte"; import {useTracker} from "meteor/rdb:svelte-meteor-data"; import Assign from "/imports/ui/Assets/Assign.svelte"; + import Assignments from "/imports/ui/Assets/Assignments.svelte"; let canManageLaptops = false; let isAdmin = false; @@ -22,6 +23,7 @@ let tabs = []; if(canManageLaptops) { + tabs.push({id: 'assignments', label: 'Assignments'}); tabs.push({id: 'assignment', label: 'Assign'}); } if(isAdmin) { @@ -43,6 +45,8 @@ {:else if activeTab && activeTab.id === 'assignment'} + {:else if activeTab && activeTab.id === 'assignments'} + {/if} diff --git a/imports/ui/Assets/Assign.svelte b/imports/ui/Assets/Assign.svelte index 02929e5..ffec3ea 100644 --- a/imports/ui/Assets/Assign.svelte +++ b/imports/ui/Assets/Assign.svelte @@ -37,6 +37,8 @@ let selectedGrade = 'All'; let selectedAssignee; let assetId = ""; + let assetIdWidget; + $: { if(selectedSiteId) { if(selectedCategory === 'Student') { @@ -52,6 +54,13 @@ } } } + + const createAssignment = () => { + if(assetId && assetId.length && selectedAssignee) { + Meteor.call("AssetAssignments.add", assetId, selectedCategory === 'Student' ? "Student" : "Staff", selectedAssignee._id) + assetId = ""; + } + }
    @@ -91,10 +100,15 @@ - - - - + +
    + + + + +
    {#if $assignees} {#each $assignees as assignee} diff --git a/imports/ui/Assets/Assignments.svelte b/imports/ui/Assets/Assignments.svelte new file mode 100644 index 0000000..95064da --- /dev/null +++ b/imports/ui/Assets/Assignments.svelte @@ -0,0 +1,133 @@ + + +
    +

    Asset Assignments

    + + + + + + + + + + + + + + + + + + +
    + + + + + + +
    + + {#each searchResults as result} + + + {result.firstName} {result.lastName} + {result.email} {result.grade ? '(' + result.grade + ')' : ""} + + + {/each} + +
    + + \ No newline at end of file