tonyzimbinski / infinite-campus

📚 unofficial API for Infinite Campus written in Node JS
GNU General Public License v3.0
30 stars 11 forks source link

`grades` property returns `undefined` #9

Closed Lathryx closed 3 years ago

Lathryx commented 3 years ago

Firstly, I'm quite honestly amazed and thankful for this API. If it weren't for this API, I was going to have to make a web scraper, and that would've taken a while! So, thank you @qwazwsx!

Now for the issue I'm having: I created a new NodeJS project on Replit, and initialized a new instance of InfiniteCampus() with the necessary information. The login was successful, and I was able to programmatically view my courses. Wonderful! However, the one most crucial part of my project, the grades, returned undefined. Here's my current script:

const InfiniteCampus = require('infinite-campus'); 

const DISTRICT = process.env['DISTRICT']; 
const STATE = process.env['STATE']; 
const IC_USER = process.env['IC_USER']; 
const IC_PASS = process.env['IC_PASS']; 

const user = new InfiniteCampus(DISTRICT, STATE, IC_USER, IC_PASS); 

user.on('ready', () => {
    console.log("Login successful!"); 

    user.getCourses().then(terms => {
        console.log(terms[0].courses[0].grades); 
    }); 
}); 

Is there any specific reason this isn't working? Maybe something on Infinite Campus changed?

Lathryx commented 3 years ago

I've been experimenting/investigating on my own, and I'm stuck at authentication (with the Infinite Campus API). I'm authenticated, but I don't know how to use my authentication to make other requests to the API. Do I make my requests inside of a .then() statement in the login request? Do I somehow put my authentication in a header somewhere? If you can help me with this, @qwazwsx, I would love to help you fix the API. :)

Come to think of it, where did you find all of this for the Infinite Campus API anyway? Is there documentation somewhere I don't know about? Where did you find information on the endpoints (i.e. /resources/portal/grades)?

qwazwsx commented 3 years ago

Hi there! I'm sorry you're having issues with my project.

When instantiating new InfiniteCampus(district, state, user, pass) it makes two requests to two login endpoints; one to get district info, and one to actually log in. Once the login comes back successfully, it emits the event 'ready'. You can listen to this event with user.on('ready', () => {//my code to run after we are logged in}). The API has all authentication stuff tucked away neatly so you don't have to deal with it. (If you want to know, after logging in, the User object stores a cookie jar in user.cookies, these cookies are automatically used when you make requests like .getCourses, .getNotifications etc).

I don't see anything wrong with your code, I made a commented version below that might help with understanding it.

// require the library
const InfiniteCampus = require('infinite-campus'); 

// read data from env vars
const DISTRICT = process.env['DISTRICT']; 
const STATE = process.env['STATE']; 
const IC_USER = process.env['IC_USER']; 
const IC_PASS = process.env['IC_PASS']; 

// instantiate User object. internally this will call _getDistrict to search and retrieve district URL, then _login to actually login and set auth tokens in private variable user.cookies. after we log in successfully it emits the event 'ready'
const user = new InfiniteCampus(DISTRICT, STATE, IC_USER, IC_PASS); 

// this sets up an event listener to listen to the 'ready' event. this is called once we are done logging in
user.on('ready', () => {
    console.log("Login successful!"); 

    // internally this makes two requests one to '/portal/roster' to get your schedule and to '/portal/grades' to actually fetch the grades. these are then combined into one object and returned to the user
    user.getCourses().then(terms => {
        console.log(terms[0].courses[0].grades); 
    }); 
}); 

So my hunch is that the first call to '/portal/roster' is successful, thus you get your courses correctly, but the call to '/portal/grades' fails somehow and results in .grades being undefined.

To help more with this issue, I'd need some additional logging info. If you could make the following changes to the index.js file located in /node_modules/infinite-campus. These changes will print out the bodies of both of the requests in getCourses. Let me know if you see any obvious errors in this output. If you don't see anything could you redact the sensitive info and send the log output my way so I can study it.

[STARTING AT LINE 232 in /node_modules/infinite-campus/index.js]
........
 getCourses () {
    return new Promise((resolve, reject) => {
      checkAuth.call(this)
      // fetch roster with placements
      request(this.district.district_baseurl + 'resources/portal/roster?_expand=%7BsectionPlacements-%7Bterm%7D%7D', (err, res, body) => {
+       console.log('/roster call debugging info:', err, body)
        errHandler.generic200.handle(err, res, body)
        let roster = JSON.parse(body)

        // fetch grades
        request(this.district.district_baseurl + 'resources/portal/grades', (err, res, body) => {
+        console.log('/grades call debugging info:', err, body)
          errHandler.generic200.handle(err, res, body)
          let grades = JSON.parse(body)

........

Also: No there is not any official documentation for the I.C. API. I spent a long time researching how the new portal version of I.C. works and writing this library based on their undocumented public API.

Let me know! :)

Lathryx commented 3 years ago

Thanks for the response, and thank you for the explanation! I didn't think of the authorization being stored in cookies. 🤦‍♂️

As for the debugging, I've tried to find a way, but I don't think editing node_modules files in Replit is possible, unfortunately. Things like Vim and Emacs aren't even recognized as commands, and after looking on the forums, the community also seems to have issues with attempting something like this. However, since it is online, I can share the Repl with you! Here it is (you can fork and edit it as you wish): https://replit.com/@Lathryx/DreamScore-BE#index.js (I could also try to copy the package over locally and edit/use it from there, since it's only one file.)

Let me know if there's something else I could try. Oh, and MAJOR props to you for spending the time to find all the information you needed for Infinite Campus' API on your own! Maybe you could even make unofficial docs for their API too? I'd be happy to help! 😄

qwazwsx commented 3 years ago

Hmm... I just took a look at your replit link. It seems that your district is using Campus.2116.4 while mine is using Campus.2044.3. Each school district uses its own self-hosted version of Infinite Campus which makes trying to maintain this library across all versions (without ever being able to test any other versions but my own district's) quite the chore.

I forked the replit link to fix some bugs and to print some logging data. Plugin your user info and run apiExperamentation.js and see what happens. It should print out the names of each course and the current grade you have in the course. If you don't get this output, uncomment line 52 and send the output my way.

https://replit.com/@qwazwsx/DreamScore-BE#apiExperimentation.js

qwazwsx commented 3 years ago

Basically, I'm assuming the issue is that your IC version formats grading info differently, causing the library to be unable to grab the grades info correctly. You mentioned in a comment that you got a URL error, but I think that was due to a bug in your code, please let me know if you still have issues getting the base_url + "resources/portal/grades" endpoint

Lathryx commented 3 years ago

My apologies for not responding sooner, I had gone to sleep.

What you've explained about the self-hosted versions per district seems hard to maintain in general (for outside developers, and Infinite Campus themselves). Do you know why they would do it this way? Also, I'd be happy to help you maintain this, I know you put a lot of work into it!

Some things you should know (about Replit): Replit is of course an online IDE. The way it used to be, you would just make a .env file as usual for environment variables, but they've recently changed it to be more user-friendly (as a UI-based thing in the left tabs). When you fork a Repl, environment variables aren't transferred, in order to keep them secure. So, you'd have to use your own credentials as secrets if you wanted to use a forked Repl (I hope that made sense). Also, Replit NodeJS projects can only run the index.js file that they start with (that's just how it works). So honestly, if you have multiple files, the best thing to do would be to make separate files for everything and export them as modules to be called/used locally within index.js (the only file that can be run). Though online IDEs have their limitations, I enjoy using them so that I can work on my projects from anywhere, use templates, easily share/collaborate, host applications (in development), etc. (unless it's something that would be better done on a computer, like mobile development).

Sorry for the rant about using Replit, moving on... I tested your program with my credentials and it worked fabulously! Thank you so much! I changed the program around to now accept the user's credentials via a prompt in the console, these values will be used instead of the environment variables so that anyone can use it without having to fork it (credentials are not stored anywhere). What exactly did you change? Or what changed between my district's and your district's data format?

The new program can be found here: https://replit.com/@Lathryx/Infinite-Campus-API-Testing#index.js

Also, as I mentioned before, I would be happy to help you in maintaining this package and expanding its usability. Would there be a better way that I could contact you?

qwazwsx commented 3 years ago

Thanks for the info about Replit! I'll for sure use this in the future.

https://replit.com/@qwazwsx/Infinite-Campus-API-Testing#index.js

I modified your Repl.it to use this API instead of making requests with axios. Let me know if it works. If it doesn't work, it's probably because your I.C. version returns data a little differently. If this is the case please let me know and I'll add support for your specific version of I.C.

One thing I changed in between the previous Repl.it's was allowing Axios to use cookies so that the cookies returned from the login request can be used in the grades request.

About the separate self-hosted versions of I.C: I'm not actually sure why they do this. On their end, they won't run into issues because the I.C. client version always matches the server version. They also probably don't expect people to use their public API in the way that this library does, so they dont design for it. There is an official I.C. API however, it's for use by schools directly (not for students), and you have to apply in order to be able to use it.

Lathryx commented 3 years ago

I apologize for being so late to respond to this, busy weekend! Oh, and you're welcome. Replit is great! I also enjoy using other things like Codepen and Codesandbox.

I tested your code, and nothing seemed to print meaning grades === undefined (still). I added an else statement and that's what printed for each course. Instead, for each course, I decided to print the course object, here's what returned (teacher names, room numbers, times, etc. have been redacted):

{
  name: '9th Lit/Comp H',
  courseNumber: '23.2610040',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464910'
}
{
  name: 'Beg Orchestra I',
  courseNumber: '53.0561000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464908'
}
{
  name: 'Beginning Music Tech',
  courseNumber: '53.0221000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464909'
}
{
  name: 'Biology',
  courseNumber: '26.0120000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464915'
}
{
  name: 'GSE Acc Alg I/Geo A H',
  courseNumber: '27.0994040',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464911'
}
{
  name: 'Gifted Participation 9',
  courseNumber: '70.2330008',
  roomName: 'N/A',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464913'
}
{
  name: 'Spanish 2',
  courseNumber: '60.0720000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464914'
}
{
  name: '9th Lit/Comp H',
  courseNumber: '23.2610040',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464910',
  placement: {
    periodName: '3',
    periodSeq: 3,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}
{
  name: 'Beg Orchestra I',
  courseNumber: '53.0561000',
  roomName: '7242',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464908',
  placement: {
    periodName: '1',
    periodSeq: 1,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}
{
  name: 'Beginning Music Tech',
  courseNumber: '53.0221000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464909',
  placement: {
    periodName: '2',
    periodSeq: 2,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}
{
  name: 'Biology',
  courseNumber: '26.0120000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464915',
  placement: {
    periodName: '5',
    periodSeq: 5,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}
{
  name: 'GSE Acc Alg I/Geo A H',
  courseNumber: '27.0994040',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464911',
  placement: {
    periodName: '4',
    periodSeq: 4,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}
{
  name: 'Gifted Participation 9',
  courseNumber: '70.2330008',
  roomName: 'N/A',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464913',
  placement: {
    periodName: '9',
    periodSeq: 9,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}
{
  name: 'Spanish 2',
  courseNumber: '60.0720000',
  roomName: 'REDACTED',
  teacher: 'REDACTED',
  grades: undefined,
  _id: '47464914',
  placement: {
    periodName: '6',
    periodSeq: 6,
    startTime: 'REDACTED',
    endTime: 'REDACTED'
  }
}

From this, we can conclude that only grades returns undefined, everything else works, meaning for some reason, I guess my grades are indexed differently than yours. Here's my guess as to what I can do: Take your source code, and add some logs to the JSON objects returned from your requests. See how it indexes on my end, and then, given that there's some variable that's returned for the IC version, we can use that to first check how the user's information is parsed when executing requests (immediately after the user is logged in). That way, depending on what version we're dealing with, the data will be parsed and returned accordingly. If this makes sense, I guess I'll try that next.

Regarding the self-hosted IC versions, even if they don't expect other people to use this API, it still doesn't make sense how they're managing their versions. It just seems more logical to keep the version consistent. I've also seen the other API, the one where you have to apply for it. Unfortunately, I don't think they'd give some random devs access just because. There seems to be a lot of information on the form that I don't have anyway (vendor's name, superintendent's name, etc.). :/

I would like to work with you on this and help with this project's documentation too. But, would there be a better way to contact you?

Lathryx commented 3 years ago

Okay, I attempted the logging/debugging from my end using the source code, and I found that the root of our problem may lie in virtual classes. In your code, you loop through the first index of the array returned from the /grades endpoint.

...
// fetch grades
        request(this.district.district_baseurl + 'resources/portal/grades', (err, res, body) => {
          errHandler.generic200.handle(err, res, body)
          let grades = JSON.parse(body)

          let result = [] // object that we return later
          let crossReference = {}

          // loop over terms from /grades
          grades[0].terms.forEach((term, i) => {
            let termResult = {
              name: term.termName,
              seq: term.termSeq,
              startDate: term.startDate,
              endDate: term.endDate,
              courses: []
            }
...

However, my grades object returns two items in its array, one for normal school, and one for virtual school.

 [
  {
    enrollmentID: 8841851,
    terms: [ [Object], [Object] ],
    schoolID: 92,
    displayName: '20-21 REDACTED High School',
    gradesEnabled: true,
    assignmentsEnabled: true,
    gradingKeyEnabled: true,
    courses: [
      [Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object]
    ]
  },
  {
    enrollmentID: 9037699,
    terms: [ [Object] ],
    schoolID: 172,
    displayName: '20-21 REDACTED Virtual SS_S1',
    gradesEnabled: true,
    assignmentsEnabled: true,
    gradingKeyEnabled: true,
    courses: [ [Object] ]
  }
]
[
  {
    name: 'S1',
    seq: 1,
    startDate: '2020-08-17',
    endDate: '2020-12-18',
    courses: [
      [Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object]
    ]
  },
  {
    name: 'S2',
    seq: 2,
    startDate: '2021-01-06',
    endDate: '2021-05-26',
    courses: [
      [Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object]
    ]
  }
]

✔️ 👍

Next, looking at the source code more, I saw you added an if statement to return undefined for "courses without grades". I added an extra log statement here.

if (!grade.progressScore 
        && !grade.progressPercent 
        && !grade.progressTotalPoints 
        && !grade.progressPointsEarned) {
            courseResult.grades = undefined;
            console.log("NO GRADES"); // LATHRYX EDITS --------------
}

I found that this statement was ran, hence why my grades are returned undefined. I've also tested this on my friend's credentials, and some of his grades are shown, and some are not (most likely because of this same issue). But why? Well, more logs! Now this time: console.log(grade); Let's see what that object looks like:

{
  _id: '122418',
  personID: 122418,
  trialID: 6383,
  calendarID: 6383,
  structureID: 6383,
  courseID: 432338,
  courseName: 'Biology',
  sectionID: 2853338,
  taskID: 4260,
  termID: 5815,
  hasAssignments: false,
  hasCompositeTasks: false,
  taskName: '6 Week Progress',
  gradedOnce: false,
  treeTraversalSeq: 9,
  calcMethod: 'nu',
  groupWeighted: true,
  usePercent: false,
  curveID: 1,
  scoreID: 9838430,
  score: '95',
  percent: 94.8,
  comments: 'Doing well.',
  hasDetail: false,
  _model: 'PortalGradingTaskModel',
  _hashCode: '-170251353',
  termName: 'S1',
  termSeq: 1
}

In your source code, you call these properties by .progressPercent, .progressScore, .progressTotalPoints, and so on. Though in my object, these properties are without the prepended progress. That's my problem! Now, all we have to do is figure out how to implement this. 😄

qwazwsx commented 3 years ago

Good catch! You did an excellent job explaining this :+1:

          grades[0].terms.forEach((term, i) => {

https://github.com/qwazwsx/infinite-campus/blob/master/index.js#L249 I see here (like you said) that I explicitly grab the first element in the array of schools you are enrolled in. I did this because, on my end, I only am enrolled in one school. I actually didn't know I.C. let you be enrolled in multiple schools. We should just be able to add a forEach on the grades array to make it traverse each school you attend. However this raises a problem, will we end up having double the Term's returned (one set for school 1 and another for school 2). We could make a new Class, School. However, this would be a breaking change. (would you call a new method, getSchools()?, would getCourses() return an array of Schools?) I'm not really sure what to do here. What do you think?


Then when it comes to the .progressPercent vs .percent I think we can use optional chaining to make this easy

grades: {
                  score: grade?.progressScore | grade?.score,
                  percent: grade?.progressPercent | grade?.percent,
                  totalPoints: grade?.progressTotalPoints | grade?.totaPoints,
                  pointsEarned: grade?.progressPointsEarned | grade?.pointsEarned
},

https://github.com/qwazwsx/infinite-campus/blob/master/index.js#L268-L273

Thanks so much for doing the research! Let me know what you think.

qwazwsx commented 3 years ago

RE: working with me on this library. That would be fantastic! In about a year or so I'll no longer have access to Infinite Campus and I'd love to have another maintainer to keep the project alive. Offtopic, but I've also been thinking about migrating this project to TypeScript once some of my other (more pressing) projects settle down.

qwazwsx commented 3 years ago

I was debating splitting this issue into another one for clarity's sake, but I'll leave it as is. Ignore all the futzing around with titles & labels. :)

Lathryx commented 3 years ago

Okay, this sounds great! I think, to keep this library as simple as possible, maybe have a separate function to return raw data, but better formatted than Infinite Campus's API (their API needs some restructuring if you ask me). Maybe something like, .getRawData() (for users with more complicated schedules)?

It's not necessarily two separate school, just that, during the pandemic I was technically a virtual student at my school, and over the summer I had taken an extra course to get out of the way, so it may be listed as a separate school, but it's still technically the same one.

I do think the user's current school will always be the first indexed in this array, but I can't confirm that. However, I'm attending yet another new school (a new STEM school in my area) next year, so I will definitely keep an eye out for how Infinite Campus indexes that! If this is true, then it should be fine to just use the first index, and have a separate function for a more detailed structure (giving the full array with all data, rather than just one school). If this is not the case, I've got no idea what the hell Infinite Campus is doing because there seems to be no pattern to their data management whatsoever, and we'll just have to figure something out.

For the grades thing, I think the optional chaining thing would work fabulously! And this way, if there happens to be yet another format this data can come in, then we can just add to it! Maybe something to make users of this library aware of, I'd add an "error message" that comes up if the grades property returns undefined (or just something in the docs) that says "If the grades property was expected to contain a value other than undefined, please contact the developer via a GitHub issue" or something like that.

Working on the library: that's wonderful! I'd be happy to help you out! I don't currently know any TypeScript, but I'm willing to learn (it would be helpful for my ReactJS work anyways).

Of course, if there's a better way to contact you, please let me know. And if you're busy with other things, no worries. This obviously isn't a high-priority library, just a helpful tool! :)

Lathryx commented 3 years ago

Here's the last Repl I was working with, by the way! 😅 https://replit.com/@Lathryx/Infinite-Campus-API-Debug-for-Grades-Undefined#source-code.js

qwazwsx commented 3 years ago

Hmm... looks like the nullish coalescing operator is node 14+, making it incompatible with all lower versions (replit uses node 12), As Node v12 is currently in Maintenance LTS, with EOL in 2022, I'd like to keep support for it.

Regarding the grades[] array. I'm still a little conflicted on how I want to handle it. I would like for this to be a non-breaking change, but I'm just not sure how. Here are a few ideas I've had

Because of all this I think that support for multiple schools is going to have be a breaking change. Let me know if you have any other ideas. This is the structure I'm thinking so far.

getCourses() returns [School, School, ...] (an array of School objects)

with the School class looking like this

    {
        name: '20-21 REDACTED High School',
        id: 123, // these ID's seem to be a unique school identifier 
        terms: [Term, Term, Term, ...] // array of Terms we build
    }

Re: Contact. I think that this Github Issue is the appropriate place to discuss this update, as other people can join in if they would like. I opened up Github Discussions on this repo and while I do like its threads feature, I think it's a bit overkill as this is essentially a one-topic dialogue. Let me know what you think.

Lathryx commented 3 years ago

Firstly, it's unfortunate that we can't use this ?? operator, which I hadn't even known about until now; that would've been very useful. I had also noticed, while working on this about 5 minutes ago, that optional chaining also doesn't seem to work on Replit, and returns some sort of Typescript error. 😢

Secondly, I seemed to get the grades array working, as seen in this Repl in ./source-code-fixed.js (scroll to the getCourses method). Here's what I changed:

// ...
grades: {
  score: (grade.progressScore !== undefined) ? grade.progressScore : grade.score,
  percent: (grade.progressPercent !== undefined) ? grade.progressPercent : grade.percent,
  totalPoints: (grade.progressTotalPoints !== undefined) ? grade.progressTotalPoints : grade.totalPoints,
  pointsEarned: (grade.progressPointsEarned !== undefined) ? grade.progressPointsEarned : grade.pointsEarned
// ...

Using an inline ternary operator if statement, this checks if the progress properties are equal to undefined. If they aren't, they'll be used, otherwise, the properties without progress in their names will be used. I noticed that some of my courses use the progress properties and some do not. If the course doesn't use them, then totalPoints and pointsEarned seemed to be left out entirely. 🤔

// grades[0].terms[0].courses.gradingTasks[0]... 
  {
    _id: '122418',
    personID: 122418,
    trialID: 6383,
    calendarID: 6383,
    structureID: 6383,
    courseID: 431924,
    courseName: '9th Lit/Comp H',
    sectionID: 2642163,
    taskID: 4260,
    termID: 5815,
    hasAssignments: false,
    hasCompositeTasks: false,
    taskName: '6 Week Progress',
    gradedOnce: false,
    treeTraversalSeq: 9,
    calcMethod: 'nu',
    groupWeighted: true,
    usePercent: false,
    curveID: 1,
    scoreID: 9838140,
    score: '90', // there's a score... 
    percent: 90, // there's a percent... 
    comments: 'Doing well.',
    hasDetail: false,
    _model: 'PortalGradingTaskModel',
    _hashCode: '-1082240492',
    termSeq: 1,
    termName: 'S1'
  } // but there is no 'totalPoints' or 'pointsEarned' properties? 

I'm not sure what to make of this, but it's an observation that I made. Let me know if you'd like me to make a pull request about this change. 🙂

Anyhow, I do like the sound of a School class. However, I'm not sure anything like that will be needed anyways. After looking at the details in the "second school," it seems to be the information for my virtual school (for a class over the summer I'm taking to get out of the way). I presume they just added it all in early. So there's not two school I'm simultaneously enrolled in, but rather, the current school I'm attending, and the next "school's" (just one summer class) information. I think the best thing to do for now until we have a more critical reason to make such a large change, is to just add an exception of some sort. Maybe an error/log message, or maybe some slight restructuring for this temporary state of dual enrollment (or both so that there's an explanation for as to why it's structured differently).

Communication: Well, I was thinking more of communication outside of just this issue specifically. I think GitHub issues work wonderfully for things like this (it's what they're designed for). However, for more general communication over the entire project (since I may be helping maintain this), something like Twitter or Discord? Let me know. 🙂

qwazwsx commented 3 years ago

Yeah, I like that workaround for the nullish coalescing operator. I'm a big can of it because it avoids issues when using | with nullish values like false or 0.

About that class without points, you can see that the property hasAssignments is set to false, some classes are just like this. It shouldn't cause any issues other than totalPoints and pointsEarned being undefined. Unless of course that class does have assignments and the API is just being weird....

Hm. Okay, I guess if having two schools becomes a problem we can create a more elegant solution. But for now, I think it's okay. I do also like the idea of throwing a warning in the console if you are enrolled in multiple schools and you don't specify one explicitly. I'll add an optional parameter, getCourses(schoolID) that can specify which school to retrieve data from. I can throw together a PR for this later today. And I'll rebuild the docs sometime as well.

Also: Please do make a PR for the progressScore vs score change. Thanks for contributing! :+1:

Lathryx commented 3 years ago

Yes, I've used this in my ReactJS development many times, it works well!

I think you are correct because I believe that was a class I was enrolled in, but turned out to not have assignments this year because of the pandemic, so I think it's alright. Thanks!

Agreed, for now, we can leave it as-is unless it's necessary to change to account for something like that; the added optional parameter would work well, too! I will definitely get a PR out for that change. Your welcome! I would also be happy to help with the docs as well. Also, would you have anything like Twitter or Discord that I could perhaps contact you on?

qwazwsx commented 3 years ago

I'm working on the PR for custom schoolID's right now. Do you want to make a PR for the progressScore change or should I include it in my getCourses schoolID parameter PR?

As for communication, I think that Discord would be best, I can send you my username later today.

qwazwsx commented 3 years ago

Here's what the code looks like right now

  getCourses(schoolID) {

...
...
...

          let schoolIndex

          // if we are enrolled in multiple schools
          if (grades.length > 1) {
            // build list of schools
            let schools = []
            grades.forEach((school) => {
              schools.push({ schoolName: school.displayName, id: school.schoolID, numberOfTerms: school.terms.length, totalNumberOfCourses: school.courses.length })
            })

            // throw warning is schoolID isn't specifed
            if (schoolID === undefined) {
              console.warn(`WARNING: You are enrolled in ${grades.length} schools, please explicitly specify a school ID to fetch courses from. Please see the below output to see which schoolID to use. (Defaulting to the first school returned by I.C. API - name: '${schools[0].schoolName}' - id: ${schools[0].id})`, schools)
              // default to first in array
              schoolIndex = 0
            } else {
              // find index from schoolID
              grades.forEach((school, i) => {
                if (school.schoolID == schoolID) {
                  schoolIndex = i
                }
              })
              if (schoolIndex === undefined) {
                throw new Error(`Supplied schoolID in getCourses() does not exist, please select from the following list \n\n ${JSON.stringify(schools)}\n\n`)
              }
            }
          }

and then we just select grades[schoolIndex] later in the code

qwazwsx commented 3 years ago

heres a replit https://replit.com/@qwazwsx/Infinite-Campus-API-getCourses-with-schoolID-parameter

Is there any way you could test this out as I am only enrolled in 1 school?

Thanks

qwazwsx commented 3 years ago

PR @ #11, please take a look

Lathryx commented 3 years ago

Okay, I think the PR is out now. You should be able to see it! 😅

Once we have all of those changes in, we should be good to go for this (for now)!

Also, Discord sounds great. I've never really done any collaboration things like this before, so thanks a ton! It's fun!