Register for InstructureCon25 • Passes include access to all sessions, the expo hall, entertainment and networking events, meals, and extraterrestrial encounters.
Found this content helpful? Log in or sign up to leave a like!
I have been working for a while, and I have had the following query to get all submissions in a course:
query MyQuery {
course(id: "22424") {
submissionsConnection {
nodes {
grade
assignment {
name
}
user {
_id
enrollments(courseId: "22424") {
type
}
}
}
}
}
}
I'm encountering an issue with my script that interacts with the Canvas API. Previously, it successfully retrieved all submissions for all assignments within a course. However, recently, the script's behavior has changed, and it now only fetches the submissions for the first assignment, instead of all of them. I've confirmed this behavior across two separate university Canvas environments, suggesting it's not an isolated issue.
My questions are:
I suspect that you're misinterpreting what is happening. It's not the first assignment that is being returned, but the first 20 items, which all happen to be from the first assignment, that is being returned.
The GraphQL API Change Log explains what happened.
In September 2024, they announced they were going to limit the results of GraphQL. In particular, you would be capped at 20 items per request by default, but up to 100 if you specify pagination. They kept putting that off, but they finally implemented it. I didn't notice as I had made the changes to my code back in September when I was freaking out (they basically gave us 1 day notice and said "we're doing this now!" but then put it off.
You need to use pagination to get all of the data.
Here's what I do.
query allSubmissions($courseId: ID!, $c1: String) {
course(id: $courseId) {
submissionsConnection(first: 100, after: $c1) {
nodes {
assignmentId
userId
score
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
Then I use query variables of:
{
"courseId": 4129666,
"c1": null
}
When you make the request, you will have this information at the end
"pageInfo": {
"hasNextPage": true,
"endCursor": "MTAw"
}
I check to make sure hasNextPage is true and then pass the endCursor as c1 in my next request. "MTAw" is the base-64 encoded string for "100".
This time, my query variables are:
{
"courseId": 4129666,
"c1": "MTAw"
}
At the end, I now have endCursor of "MjAw", which is the base-64 encoded string for "200".
We keep this up until the hasNextPage is false.
You'll notice that I didn't put the enrollment type in the request like you did. Then enrollments API in the REST API is costly. It uses bookmark pagination and takes a lot longer to run than most requests. I avoid it when possible. My request for 100 grades at a time took 569 ms. When I changed it to use your request, it took 630 ms. Those times are relatively close and so it may not be as big of issue with GraphQL However, the enrollment data doesn't change while you're making this request so there's no need for me to request it with each submission.
You could use the enrollmentsConnection outside of the submissionsConnection as part of the same GraphQL request. However, I can only specify up to 100 students at a time. That's great for me, where I had 20 students in a class, but doesn't work for everyone. What I would do is make a separate request to get the enrollments. That allows me to get all of the user ids and enrollment types once rather than sending it with each request. I then have to do some logic in my code.
This also allows me to avoid the enrollments object, which can have multiple records for each user. Instead, I specify that I just want students in my list of users.
query enrollments($courseId: ID!, $c1: String) {
course(id: $courseId) {
usersConnection(
filter: {enrollmentTypes: StudentEnrollment, enrollmentStates: active}
first: 100
after: $c1
) {
nodes {
_id
sisId
sortableName
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
Accompany this by the query variables:
{
"courseId": 4129666,
"c1": "null"
}
If you have more than 100 students, you'll have the hasNextPage be true and you can make the next call. You'll get an endCursor either way. In this case, mine was MTg, which is the base-64 encoding of 18, the number of students I ended with.
That request took 291 ms, which I will more than make up since I have lots of submissions in that class.
Of note, the GraphQL submissions only returns submission records when there is a submission entry. If a student hasn't made a submission or there isn't a grade, then it doesn't return anything for that student. This is another reason why I get the list of students first. If a student should have submitted the assignment, but hasn't, then I don't get the record through GraphQL. However, if I get a list of all students and all assignments separately, then I can fill in the missing grades with 0's.
Alternatively, you can specify the submission state you want. Choices include submitted, unsubmitted, pending_review, graded, ungraded, and deleted. If you keep on top of grading, a state of submitted my not have much in it. For me, most of the values were graded. Those are included, but the unsubmitted and deleted states would not show up by default.
Modify the submissionsConnection() line to include the filter.
submissionsConnection(first: 100, after: $c1, filter: {states: unsubmitted})
Through the graphQL interface, you can only select one state at a time. However, it will take an array of states. You'll have to modify that within the code editor. For example, here is code to return even the missing submission records.
query allSubmissions($courseId: ID!, $c1: String) {
course(id: $courseId) {
submissionsConnection(
first: 100
after: $c1
filter: {states: [submitted, unsubmitted, pending_review, graded, ungraded]}
) {
nodes {
assignmentId
score
userId
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
I typically make two requests if needed. The default, without filtering on state, which gives me all the existing grades. Then, if I need to get the unsubmitted ones, I make a separate API call for those. I don't always need them, but I get to reuse code that way.
By the way, the c1 parameter is just because it was my first constant. If I needed it, I used c2. Instead of using a useful name like "endCursor" or "lastSubmission", I just called it c1. That allowed my generic code that made the API call for the GraphQL to identify that it was a pagination variable and which one it was. My other variables had specific names, but all pagination variables were c followed by a number. The problem is that you may need to have more than one level of pagination, but you can only have "endCursor" once. In the end, I restructured my queries to only use one level of pagination (assuming that I will never have more than 100 of certain things like assignment groups). This allowed me to easily handle pagination without having to load an external library and incorporating all the bloat that it would bring.
Thanks so much! Now I understand. I will get to work on this right away because I have a lot of queries to change.
Have you seen anyway to get a list of students who have started the submission like attached a file, add text, etc but not submitted? One Of our current issues is students who upload a file but miss the last step of submitting it.
They go back into the submission point see what they expect to see as their submission but still miss the submit button at the bottom. So I'm guessing the attempt is saved somewhere, it does not appear to be in the submission.body.
To interact with Panda Bot in the Instructure Community, you need to sign up or log in:
Sign In