This document is to capture my initial attempts to transact data with the Queensland University of Technology (QUT) Canvas Learning Management System (LMS) via the Canvas LMS representational state transfer (REST) web application programming interface (API). I want to use R because it’s the language I know best, it is well suited to working with data, and many of the assignments I want to work with are in R.
I’m particularly keen to figure out how to
GET /api/schema/latest”rcanvas may be useful and……I’m short on time!
I was going to go with the httr2 and
jsonlite packages to roll my own Canvas requests, but with
time running out to get DSB100 assessments marked, I decided to see how
far I could get with rcanvas first, installing it from the
console using
What I’ve learned is that rcanvas is useful, but a bit
patchy. There are bugs, missing features, etc. The right thing to do
would be to contribute to the package’s development on github, but
again, time is short so I am adopting some stopgap solutions for the
moment.
As described here
You must first safely stash your Canvas API token into your operating system’s keyring. This only needs to be done once. To obtain the Canvas API token follow this trail in Canvas:
Canvas -> Account -> Settings -> Approved Integrations -> Add new token
You then need to run the following once in the R console:
set_canvas_token("YOUR_TOKEN_HERE")
I did this, and also saved the token I got from Canvas in my
.Renviron file as described here.
Note
that we need to tell rcanvas what Canvas domain we are
using. Here’s how for the QUT domain:
At one point, R started producing this error on startup:
Error in exists(cacheKey, where = .rs.WorkingDataEnv, inherits = FALSE) :
invalid first argument
It was reassuring to see rcanvas::get_course_list() work
first time
| id | course_code | name |
|---|---|---|
| 4089 | DSB100_21se2 | DSB100_21se2 Fundamentals of Data Science |
| 12878 | DSB100_22se2 | DSB100_22se2 Fundamentals of Data Science |
| 14567 | DSB100_23se2 | DSB100_23se2 Fundamentals of Data Science |
| 2792 | EGH400-1_23se1 | EGH400-1_23se1 Research Project 1 |
| 14584 | EGH400-1_23se2 | EGH400-1_23se2 Research Project 1 |
| 2796 | EGH400-2_23se1 | EGH400-2_23se1 Research Project 2 |
| 14585 | EGH400-2_23se2 | EGH400-2_23se2 Research Project 2 |
| 14587 | EGH408_23se2 | EGH408_23se2 Research Project |
| 14824 | IFN657_23se2 | IFN657_23se2 Principles of Software Security |
| 2927 | IFN703_23se1 | IFN703_23se1 Advanced Project |
| 14843 | IFN703_23se2 | IFN703_23se2 Advanced Project |
| 6034 | IFN704_21se1 | IFN704_21se1 Advanced Project 2 |
| 5821 | IFN704_21se2 | IFN704_21se2 Advanced Project 2 |
| 5824 | IFN704_22se1 | IFN704_22se1 Advanced Project 2 |
| 12674 | IFN704_22se2 | IFN704_22se2 Advanced Project 2 |
| 2929 | IFN704_23se1 | IFN704_23se1 Advanced Project 2 |
| 14616 | IFN704_23se2 | IFN704_23se2 Advanced Project 2 |
| 14635 | IFN712_23se2 | IFN712_23se2 Research in IT Practice |
| 350 | LTU_ITC100 | Introduction to Canvas - For Coordinators |
| 123 | LTU_AdvancedPageElements | LTU Canvas site styling resources |
| 10245 | Sandpit_lovelldr | Sandpit_lovelldr |
| 10375 | Training001_lovelldr | Training001_lovelldr |
idsCanvas uses two different kinds identifiers to specify different pieces of data::
_id,
a.k.a. legacy or traditional identifiers
idSince the numeric identifiers are all over the place in
rcanvas, I wrote some helper functions to work with
them.
list_courses()list_courses <- function(regex="."){
get_course_list() |>
select(id, course_code, name) |>
filter(str_detect(name, regex))
}…lists course_id and name pairs of names
that contain an optional regular expression, e.g.,
list_courses("DSB"):
| id | course_code | name |
|---|---|---|
| 4089 | DSB100_21se2 | DSB100_21se2 Fundamentals of Data Science |
| 12878 | DSB100_22se2 | DSB100_22se2 Fundamentals of Data Science |
| 14567 | DSB100_23se2 | DSB100_23se2 Fundamentals of Data Science |
get_course_id_of(regex=".")get_course_id_of <- function(regex="."){
get_course_list() |>
select(id, name) |>
filter(str_detect(name, regex)) |>
pull(id)
}…yields the course_ids of courses that I have access to
whose name(s) match an optional regular expression
[1] 4089 12878 14567 2792 14584 2796 14585 14587 14824 2927 14843 6034
[13] 5821 5824 12674 2929 14616 14635 350 123 10245 10375
[1] 14567
[1] 10245
get_course_folders(course_id)…returns information about all file folders for the specified
course_id:
get_course_folders <- function(course_id){
get_course_items(course_id = course_id, item="folders")
}Rows: 4
Columns: 22
$ id <int> 603765, 603766, 176869, 698585
$ name <chr> "Icons", "Uploaded Media", "course files", "tmp"
$ full_name <chr> "course files/Icons", "course files/Uploaded Media", …
$ context_id <int> 10245, 10245, 10245, 10245
$ context_type <chr> "Course", "Course", "Course", "Course"
$ parent_folder_id <int> 176869, 176869, NA, 176869
$ created_at <chr> "2022-06-23T22:44:02Z", "2022-05-26T02:13:00Z", "2022…
$ updated_at <chr> "2023-08-23T03:29:49Z", "2023-08-23T03:29:49Z", "2022…
$ lock_at <lgl> NA, NA, NA, NA
$ unlock_at <lgl> NA, NA, NA, NA
$ position <int> 2, 1, NA, 30
$ locked <lgl> FALSE, FALSE, FALSE, FALSE
$ folders_url <chr> "https://canvas.qut.edu.au/api/v1/folders/603765/fold…
$ files_url <chr> "https://canvas.qut.edu.au/api/v1/folders/603765/file…
$ files_count <int> 188, 192, 0, 2
$ folders_count <int> 0, 0, 3, 0
$ hidden <lgl> NA, TRUE, NA, NA
$ locked_for_user <lgl> FALSE, FALSE, FALSE, FALSE
$ hidden_for_user <lgl> FALSE, FALSE, FALSE, FALSE
$ for_submissions <lgl> FALSE, FALSE, FALSE, FALSE
$ can_upload <lgl> TRUE, TRUE, TRUE, TRUE
$ course_id <int> 10245, 10245, 10245, 10245
list_course_folders(course_id, regex=".")…lists folder_id and full_name pairs for
full_names that contain a given regular expression:
list_course_folders <- function(course_id, regex="."){
get_course_folders(course_id) |>
select(id, full_name, parent_folder_id) |>
filter(str_detect(full_name, regex))
} id full_name parent_folder_id
1 603765 course files/Icons 176869
2 603766 course files/Uploaded Media 176869
3 176869 course files NA
4 698585 course files/tmp 176869
get_folder_id_of(course_id, name=NA, full_name=NA)…returns the folder_id of a folder with the specified
name or full_name
get_folder_id_of <- function(course_id, name=NA, full_name=NA){
if(!is.na(name)){
get_course_folders(course_id) |>
select(id, name, full_name) |>
filter(.env$name==name) |> pull(id)
}
else if (!is.na(full_name)){
get_course_folders(course_id) |>
select(id, name, full_name) |>
filter(.env$full_name==full_name) |> pull(id)
}
}No names, no return value.
[1] 176869
[1] 176869
[1] 603765
rcanvas issuesrcanvas::create_course_folder()make_canvas_url on line 2 should not have
the first argument canvas_url()create_course_folder <- function(course_id, name, parent_folder_id = NULL) {
url <- make_canvas_url(canvas_url(), "courses", course_id, "folders")
args <- sc(list(name = name,
parent_folder_id = parent_folder_id))
invisible(canvas_query(url, args, "POST"))
message(sprintf("Folder %s created", name))
}Here is a quick fix. Note that Canvas adds a unique ascending digit to the folder name if the folder already exists.
my.create_course_folder <- function (course_id, name, parent_folder_id = NULL)
{
url <- rcanvas:::make_canvas_url("courses", course_id, "folders")
args <- rcanvas:::sc(list(name = name, parent_folder_id = parent_folder_id))
message(sprintf("Folder %s created", name))
invisible(rcanvas:::canvas_query(url, args, "POST"))
}Here’s the initial set of folders in my Sandpit:
id full_name parent_folder_id
1 603765 course files/Icons 176869
2 603766 course files/Uploaded Media 176869
3 176869 course files NA
4 698585 course files/tmp 176869
This is the first section of code that changes anything on Canvas.
eval=FALSEparent_folder_id == NULL
/course files/unfiled/course_idHere’s how to create course files/tmp
my.create_course_folder(
course_id = Sandpit,
name = "tmp",
parent_folder_id = get_folder_id_of(Sandpit, "course files")
)Note that the following this creates
/course files/tmp_tmp//course files/tmp/tmp/ as the message suggests…I’m nervous about the destructive potential of programmatic deletion, so I am using the Canvas web interface to delete files and folders at this point.
I’m not sure whether rcanvas supports the retrieval of
files from specific folders.
get_course_items(course_id, item, include = NULL) will
return data about various kinds of items
item="files" yields a lot of data for just my humble
sandpit:# A tibble: 382 × 24
id uuid folder_id display_name filename upload_status `content-type`
<int> <chr> <int> <chr> <chr> <chr> <chr>
1 2967461 8yFTrlw… 698585 ./tmp/below… .%2Ftmp… success text/plain
2 2708042 iDZ8Rws… 603766 alarm-excla… alarm-e… success image/png
3 2708036 UN6utaU… 603766 alarm-excla… alarm-e… success image/png
4 2708037 9y6ZwQ3… 603766 alarm-excla… alarm-e… success image/png
5 2708038 GQBhZcL… 603766 alarm-excla… alarm-e… success image/png
6 2708043 VVDgpBM… 603766 anchor_009F… anchor_… success image/png
7 2708044 ckaLG0B… 603766 anchor_012A… anchor_… success image/png
8 2708046 873yBp4… 603766 anchor_C702… anchor_… success image/png
9 2708045 V91aJF7… 603766 anchor_EFCB… anchor_… success image/png
10 2708111 gW5w3xB… 603766 arrow-down-… arrow-d… success image/png
# ℹ 372 more rows
# ℹ 17 more variables: url <chr>, size <int>, created_at <chr>,
# updated_at <chr>, unlock_at <lgl>, locked <lgl>, hidden <lgl>,
# lock_at <lgl>, hidden_for_user <lgl>, thumbnail_url <chr>,
# modified_at <chr>, mime_class <chr>, media_entry_id <lgl>, category <chr>,
# locked_for_user <lgl>, visibility_level <chr>, course_id <int>
get_course_items()I’m not sure what the include argument does. And I’m not
sure what url is actually getting passed to Canvas. So I’m
going to write some code to try to get some insight.
The action seems to be happening in
get_course_items() which calls
process_response() which calls
canvas_query() which is where the url is
formed and sent to Canvaspaginate() then ensures that the (potentially very
long) response is compiledSo my strategy is to create my own versions of
get_course_items(), process_response() and
canvas_query() that I can instrument and figure out what’s
going on.
my.canvas_query()…is adapted from rcanvas/R/utils.R:
my.canvas_query <- function(urlx, args = NULL, type = "GET", verbose=FALSE) {
args <- rcanvas:::sc(args)
resp_fun_args <- list(
url = urlx,
httr::user_agent("rcanvas - https://github.com/daranzolin/rcanvas"),
httr::add_headers(Authorization = paste("Bearer", rcanvas:::check_token()))
)
if (type %in% c("POST", "PUT"))
resp_fun_args$body = args
else
resp_fun_args$query = args
if(verbose){
cat("> [my.canvas_query] str(resp_fun_args))\n")
str(resp_fun_args)
}
resp <- do.call(type, resp_fun_args, envir=rcanvas:::cdenv)
httr::stop_for_status(resp)
resp
}my.process_response()…is adapted from rcanvas/R/process_response.R
my.get_course_items()…is adapted from rcanvas/R/course-data.R
my.get_course_items <- function(course_id, item, include = NULL, verbose=FALSE) {
valid_items <- c(
"settings", "discussion_topics", "todo", "enrollments", "users", "students",
"features", "assignments", "files", "modules", "front_page", "pages", "quizzes",
"folders", "assignment_groups"
)
if (!missing(item) && !item %in% valid_items) {
stop(paste("item argument must be one of:", paste(valid_items, collapse = ", ")))
}
if (!missing(item)) {
url <- rcanvas:::make_canvas_url("courses", course_id, item)
} else {
#Omitting the item argument will return general information about the course
url <- rcanvas:::make_canvas_url("courses", course_id)
}
args <- list(per_page = 100)
include <- rcanvas:::iter_args_list(include, "include[]")
args <- c(args, include)
my.process_response(url, args, verbose=verbose) %>%
dplyr::mutate(course_id = course_id)
}I’ve set eval=FALSE to ensure that my API key is not
revealed when I knit this.
> [my.canvas_query] str(resp_fun_args))
List of 4
$ url : chr "https://canvas.qut.edu.au/api/v1/courses/10245/files"
$ :List of 7
..$ method : NULL
..$ url : NULL
..$ headers : NULL
..$ fields : NULL
..$ options :List of 1
.. ..$ useragent: chr "rcanvas - https://github.com/daranzolin/rcanvas"
..$ auth_token: NULL
..$ output : NULL
..- attr(*, "class")= chr "request"
$ :List of 7
..$ method : NULL
..$ url : NULL
..$ headers : Named chr "Bearer <My secret API key>"
.. ..- attr(*, "names")= chr "Authorization"
..$ fields : NULL
..$ options : NULL
..$ auth_token: NULL
..$ output : NULL
..- attr(*, "class")= chr "request"
$ query:List of 1
..$ per_page: num 100
These results tell me that
https://canvas.qut.edu.au/api/v1/courses/10245/files
GET /api/v1/courses/:course_id/filesGET /api/v1/folders/:id/files…so let’s try putting a file into a folder, then getting the
folder_id
upload_course_file(Sandpit,"CanvasAPI-BabySteps.Rmd",parent_folder_path = "/tmp")
(folder_id <- get_folder_id_of(Sandpit, full_name="course files/tmp"))…then making a url endpoint for that
folder_id and downloading information about the files it
contains:
my.get_folder_files(folder_id)my.get_folder_files <- function(folder_id) {
url <- rcanvas:::make_canvas_url("folders", folder_id, item="files")
args <- list(per_page = 100)
my.process_response(url, args) %>%
dplyr::mutate(folder_id = folder_id)
}et voila!
I’m nervous about the destructive potential of programmatic deletion, so I am using the Canvas web interface to delete files and folders at this point.
rcanvasrcanvas::upload_course_file()rcanvas::upload_course_file() has some issues which we
demonstrate here by:
CanvasAPI-BabySteps.Rmd) to
course files/tmp./CanvasAPI-BabySteps.Rmd) to
course files/tmpupload_course_file(Sandpit, "CanvasAPI-BabySteps.Rmd",parent_folder_path = "/tmp")
upload_course_file(Sandpit,"./CanvasAPI-BabySteps.Rmd",parent_folder_path = "/tmp")Looking at the files in course files/tmp shows that
rcanvas::upload_course_file() has used the local file path
as the destination filename and
display_name:
get_folder_id_of(Sandpit, full_name="course files/tmp") |>
my.get_folder_files() |>
select(folder_id, id, filename, display_name)I want to be able to upload files from any local path to the desired
Canvas folder so that their filename and
display_name are the local files’ basename,
e.g.,
So my strategy is to create my own version of
upload_course_file() to achieve that.
rcanvas::upload_course_file() calls
rcanvas::upload_file() which callsrcanvas::upload_content()my.upload_file()…adapted from rcanvas/R/uploads.R:
my.upload_file <- function(
url, local_file_path,
parent_folder_id = NULL, parent_folder_path = "/",
on_duplicate = "overwrite",
verbose=FALSE
) {
if (!is.null(parent_folder_id) && !is.null(parent_folder_path))
stop("Do not specify both parent folder id and parent folder path.")
file_size <- file.info(local_file_path)$size
args <- rcanvas:::sc(
list(
name = local_file_path,
size = file_size,
parent_folder_id = parent_folder_id,
parent_folder_path = parent_folder_path,
on_duplicate = on_duplicate
)
)
upload_resp <- my.canvas_query(url, args, "POST", verbose = verbose)
upload_content <- httr::content(upload_resp)
upload_url <- upload_content$upload_url
upload_params <- upload_content$upload_params
upload_params <- append(
upload_params,
list(file = httr::upload_file(local_file_path))
)
message(sprintf("File %s uploaded", local_file_path))
if(verbose){
cat("> [my.upload_file] str(upload_url)\n"); str(upload_url, nchar.max = 128)
cat("> [my.upload_file] str(upload_params)\n"); str(upload_params, nchar.max = 256)
}
invisible(httr::POST(url = upload_url, body = upload_params))
}my.upload_course_file()…adapted from rcanvas/R/uploads.R:
my.upload_course_file <- function(
course_id, local_file_path,
parent_folder_id = NULL,
parent_folder_path = "/",
on_duplicate = "overwrite",
verbose=FALSE) {
url <- rcanvas:::make_canvas_url("courses", course_id, "files")
my.upload_file(
url, local_file_path,
parent_folder_id, parent_folder_path, on_duplicate, verbose=verbose
)
}I’m nervous about the destructive potential of programmatic deletion, so I am using the Canvas web interface to delete files and folders at this point.
Let’s make a test file to upload
dir.create("./tmp", showWarnings = FALSE)
dir.create("./tmp/below", showWarnings = FALSE)
cat("Test file from CanvasAPI-BabySteps.Rmd\n", file = "./tmp/below/test.txt")If we run the following command
my.upload_course_file(
Sandpit,
"./tmp/below/test.txt",
parent_folder_path = "/tmp",
verbose = TRUE
)we get the following output (with API key obscured):
> [my.canvas_query] str(resp_fun_args))
List of 4
$ url : chr "https://canvas.qut.edu.au/api/v1/courses/10245/files"
$ :List of 7
..$ method : NULL
..$ url : NULL
..$ headers : NULL
..$ fields : NULL
..$ options :List of 1
.. ..$ useragent: chr "rcanvas - https://github.com/daranzolin/rcanvas"
..$ auth_token: NULL
..$ output : NULL
..- attr(*, "class")= chr "request"
$ :List of 7
..$ method : NULL
..$ url : NULL
..$ headers : Named chr "Bearer <My secret API key>"
.. ..- attr(*, "names")= chr "Authorization"
..$ fields : NULL
..$ options : NULL
..$ auth_token: NULL
..$ output : NULL
..- attr(*, "class")= chr "request"
$ body:List of 4
..$ name : chr "./CanvasAPI-BabySteps.Rmd"
..$ size : num 21048
..$ parent_folder_path: chr "/tmp"
..$ on_duplicate : chr "overwrite"
File ./CanvasAPI-BabySteps.Rmd uploaded
> [my.upload_file] str(upload_url)
chr "https://inst-fs-syd-prod.inscloudgate.net/files?token=eyJ0eXA"| __truncated__
> [my.upload_file] str(upload_params)
List of 3
$ filename : chr "./CanvasAPI-BabySteps.Rmd"
$ content_type: chr "unknown/unknown"
$ file :List of 3
..$ path: chr "C:\\Users\\<user id>\\<path>\\Canvas\\Canvas API\\CanvasAPI-BabySteps.Rmd"
..$ type: chr "text/x-markdown"
..$ name: NULL
..- attr(*, "class")= chr "form_file"
So it looks like the file names are set in the call to
my.canvas_query() in resp_fun_args$body
my.upload_file <- function(
url, local_file_path,
parent_folder_id = NULL, parent_folder_path = "/",
on_duplicate = "overwrite",
verbose=FALSE
) {
if (!is.null(parent_folder_id) && !is.null(parent_folder_path))
stop("Do not specify both parent folder id and parent folder path.")
file_size <- file.info(local_file_path)$size
name <- basename(local_file_path)
args <- rcanvas:::sc(
list(
name = name, # Try this!
size = file_size,
parent_folder_id = parent_folder_id,
parent_folder_path = parent_folder_path,
on_duplicate = on_duplicate
)
)
upload_resp <- my.canvas_query(url, args, "POST", verbose = verbose)
upload_content <- httr::content(upload_resp)
upload_url <- upload_content$upload_url
upload_params <- upload_content$upload_params
upload_params <- append(
upload_params, list(file = httr::upload_file(local_file_path))
)
message(sprintf("File %s uploaded", name))
if(verbose){
cat("> [my.upload_file] str(upload_url)\n"); str(upload_url, nchar.max = 128)
cat("> [my.upload_file] str(upload_params)\n"); str(upload_params, nchar.max = 256)
}
invisible(httr::POST(url = upload_url, body = upload_params))
}Let’s make another test file to upload
rcanvas::get_assignment_list() works straight out of the
box with the one test assignment I created in my sandpit
Rows: 1
Columns: 65
$ id <int> 152713
$ description <chr> "<link rel=\"stylesheet\" href=\"…
$ due_at <lgl> NA
$ unlock_at <lgl> NA
$ lock_at <lgl> NA
$ points_possible <dbl> 10
$ grading_type <chr> "points"
$ assignment_group_id <int> 36540
$ grading_standard_id <lgl> NA
$ created_at <chr> "2023-09-29T22:12:10Z"
$ updated_at <chr> "2023-09-29T22:12:10Z"
$ peer_reviews <lgl> FALSE
$ automatic_peer_reviews <lgl> FALSE
$ position <int> 2
$ grade_group_students_individually <lgl> FALSE
$ anonymous_peer_reviews <lgl> FALSE
$ group_category_id <lgl> NA
$ post_to_sis <lgl> FALSE
$ moderated_grading <lgl> FALSE
$ omit_from_final_grade <lgl> FALSE
$ intra_group_peer_reviews <lgl> FALSE
$ anonymous_instructor_annotations <lgl> FALSE
$ anonymous_grading <lgl> FALSE
$ graders_anonymous_to_graders <lgl> FALSE
$ grader_count <int> 0
$ grader_comments_visible_to_graders <lgl> TRUE
$ final_grader_id <lgl> NA
$ grader_names_visible_to_final_grader <lgl> TRUE
$ allowed_attempts <int> -1
$ annotatable_attachment_id <lgl> NA
$ hide_in_gradebook <lgl> FALSE
$ secure_params <chr> "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1…
$ lti_context_id <chr> "0609db8e-1d32-4e20-bc59-4003eaab…
$ course_id <int> 10245
$ name <chr> "Test Assignment"
$ submission_types <list> "online_text_entry"
$ has_submitted_submissions <lgl> TRUE
$ due_date_required <lgl> FALSE
$ max_name_length <int> 255
$ in_closed_grading_period <lgl> FALSE
$ graded_submissions_exist <lgl> FALSE
$ is_quiz_assignment <lgl> FALSE
$ can_duplicate <lgl> TRUE
$ original_course_id <lgl> NA
$ original_assignment_id <lgl> NA
$ original_lti_resource_link_id <lgl> NA
$ original_assignment_name <lgl> NA
$ original_quiz_id <lgl> NA
$ workflow_state <chr> "published"
$ important_dates <lgl> FALSE
$ muted <lgl> TRUE
$ html_url <chr> "https://canvas.qut.edu.au/course…
$ has_overrides <lgl> FALSE
$ needs_grading_count <int> 1
$ sis_assignment_id <lgl> NA
$ integration_id <lgl> NA
$ published <lgl> TRUE
$ unpublishable <lgl> FALSE
$ only_visible_to_overrides <lgl> FALSE
$ locked_for_user <lgl> FALSE
$ submissions_download_url <chr> "https://canvas.qut.edu.au/course…
$ post_manually <lgl> FALSE
$ anonymize_students <lgl> FALSE
$ require_lockdown_browser <lgl> FALSE
$ restrict_quantitative_data <lgl> FALSE
…and gives a lovely
get_assignment_list(DSB100_23se2) |>
select(id, name, due_at) |> kbl() |> kable_minimal(full_width = F)| id | name | due_at |
|---|---|---|
| 145929 | DSB100: Assessment 1 due | 2023-09-15T13:59:59Z |
| 149838 | DSB100: Assessment 2 due | 2023-10-09T13:59:59Z |
| 149839 | DSB100: Assessment 3 due | 2023-11-04T03:00:00Z |
Rows: 1
Columns: 31
$ id <int> 4627314
$ body <chr> "<link rel=\"stylesheet\" href=\"http…
$ url <lgl> NA
$ grade <lgl> NA
$ score <lgl> NA
$ submitted_at <chr> "2023-09-29T22:13:31Z"
$ assignment_id <int> 152713
$ user_id <int> 74948
$ submission_type <chr> "online_text_entry"
$ workflow_state <chr> "submitted"
$ grade_matches_current_submission <lgl> TRUE
$ graded_at <lgl> NA
$ grader_id <lgl> NA
$ attempt <int> 2
$ cached_due_date <lgl> NA
$ excused <lgl> NA
$ late_policy_status <lgl> NA
$ points_deducted <lgl> NA
$ grading_period_id <lgl> NA
$ extra_attempts <lgl> NA
$ posted_at <lgl> NA
$ redo_request <lgl> FALSE
$ custom_grade_status_id <lgl> NA
$ sticker <lgl> NA
$ late <lgl> FALSE
$ missing <lgl> FALSE
$ seconds_late <int> 0
$ entered_grade <lgl> NA
$ entered_score <lgl> NA
$ preview_url <chr> "https://canvas.qut.edu.au/courses/10…
$ course_id <int> 10245
The GraphiQL interface to Canvas looks pretty useful. Try this:
…and you should see something like
{
"data": {
"allCourses": [
{
"_id": "4089",
"id": "Q291cnNlLTQwODk=",
"name": "DSB100_21se2 Fundamentals of Data Science"
},
{
"_id": "12878",
"id": "Q291cnNlLTEyODc4",
"name": "DSB100_22se2 Fundamentals of Data Science"
},
...
]
}
}I haven’t figured out the syntax for selecting specific objects but
returns
{
"data": {
"legacyNode": {
"_id": "14567",
"id": "Q291cnNlLTE0NTY3",
"name": "DSB100_23se2 Fundamentals of Data Science"
}
}
}Alas! No time to spend on this now… must press on!