knitr::opts_chunk$set(echo = TRUE)

Here we present a method of using Canvas, R, and Python to extract indicators of student learning in the forms of gradebooks and rubrics. The following demonstration is a sample of the amount of coding required for this. A high level API token would make this easier because it could allow access to multiple instructors’ data and require one iteration of pogramming for gradebooks and one for rubrics.

Start by using R to extract gradebook data from Canvas

library(rcanvas)
library(tidyverse)

Get a list of courses from and then grade and analytics data from an instructor named Paul.

api_key <- readLines("canvas.txt")
set_canvas_token(api_key)
set_canvas_domain("https://uwyo.instructure.com/") 

paul1<-get_course_list()

paul1<- as.data.frame(paul1)%>%
   select(id, name, start_at)%>%
   mutate(prof = "Paul")

paul1$date<- substr(paul1$start_at,1,10)
paul1$date<- as.Date(paul1$date)

paul<-na.omit(paul1[paul1$date >= "2019-07-31", ])

paul$id<- as.character(paul$id)

write.csv(paul, "paul.csv")

assignments<- get_assignment_list(541242)


analytics<- get_course_analytics_data(582045)

grades<- get_course_gradebook(582045)

grades2<- as.data.frame(grades)%>%
  select( user_id, score,id, grade, assignment_id, entered_score, user.name, course_id, assignment_name)

project<- grades2%>%
  subset(assignment_name == "Play Therapy Theories Research Paper (Final Research Paper)")

write.csv(project, "projectgrades.csv")

Get API credentials and data from an instructor named Mark using a different .txt file.

api_key2 <- readLines("canvas2.txt")
set_canvas_token(api_key2)
set_canvas_domain("https://uwyo.instructure.com/") 

mark1<-get_course_list()

mark<- as.data.frame(mark1)%>%
   select(id, name, start_at)%>%
   mutate(prof = "Mark")

mark$date<- substr(mark$start_at,1,10)
mark$date<- as.Date(mark$date)

mark<- mark[mark$date >= "2019-07-31", ]

markgrades1<- get_course_gradebook(course_id = 539980)%>%
  select(-c(url, attachments, discussion_entries))%>%
  mutate(term = "20/FA")%>%
  mutate(course = "EDRE_5640")

markgrades2<- get_course_gradebook(course_id = 539982)%>%
  select(-c(url, attachments, discussion_entries))%>%
  mutate(term = "20/FA")%>%
  mutate(course = "EDRE_5645")

markdemomaster<- rbind(markgrades1, markgrades2)

write.csv(markdemomaster, "markdemomaster.csv")

markanalytics1<-get_course_analytics_data(539980, type = "assignments")%>%
  select(assignment_id, title, max_score)

markanalytics2<- get_course_analytics_data(539982, type = "assignments")%>%
  select(assignment_id, title, max_score)

markanalyticsmaster<- rbind(markanalytics1, markanalytics2)

markdemomaster<- left_join(markdemomaster, markanalyticsmaster, by = "assignment_id")%>%
  mutate(percent = score/max_score)%>%
  mutate(professor="Mark")%>%
  mutate(grade = case_when(
               percent <= .59 ~ "F",
               percent >=.6 & percent <=.69 ~ "D", 
               percent >=.7 & percent <=.79 ~ "C",
               percent >=.8 & percent <=.89 ~ "B", 
               percent >=.9 & percent <=2.0 ~ "A", ))%>%
    select(id, professor, user.name, course_id, term, course, assignment_name, assignment_id,  score, max_score, percent, grade)

write.csv(markdemomaster, "markdemogrades.csv")

Munge the R gradebook data into one file. This is a demonstration file. The Grades and analtyics process will require multiple iterations.

KPIJoin<- read.csv("KPIJoin.csv")%>%
  select(-assignment_id)

gradesmaster<- left_join(markdemomaster, mark, by = c("course_id" = "id"))%>%
  mutate(numbergrade = case_when(grade == "A" ~ 4,
                                 grade == "B" ~ 3,
                                 grade == "C" ~ 2,
                                 grade == "D" ~ 1,
                                 grade == "F" ~ 0))%>%
  drop_na(course_id)%>%
  drop_na(numbergrade)%>%
  mutate(concatenate =gsub(" ", "",paste(term,course,assignment_id)))

gradesmaster<- left_join(gradesmaster, KPIJoin, by = "concatenate")

write.csv(gradesmaster, "gradescleaned1.csv")

We are now moving to the python realm to begin the process of extracting data into rubrics.

Load this library and indicate your directory.

library(reticulate)
 use_python("C:/Users/mperki17/AppData/Local/Programs/Python/Python312/python.exe")

We need another knit setup session for python

knitr::opts_chunk$set(echo = TRUE, error=TRUE)

Now switch to python programming and install these packages.

!pip install canvasapi
!pip install rpytools
!pip install future-fstrings
!pip install pandas
!pip install numpy
!pip install google-colab
!pip install --upgrade google-api-python-client
## invalid syntax (<string>, line 1)

Here we extract data from an indivdual course given an assignment. The API key is hidden on a .txt file that python locates

from canvasapi import Canvas
from canvasapi.assignment import (Assignment, AssignmentExtension, AssignmentGroup,
AssignmentOverride,)
from canvasapi.rubric import Rubric, RubricAssociation
from canvasapi.submission import GroupedSubmission, Submission
import pandas as pd

# Read access token from a file
with open('canvas.txt', 'r') as file:
    PERSONAL_ACCESS_TOKEN = file.readline().strip()


# Other Canvas API and assignment setup
CANVAS_URL = 'https://uwyo.instructure.com/'
COURSE_ID = '541242'
ASSIGNMENT_ID = '4692853'

# initiate CanvasAPI library
# get course, assignment with rubric settings
canvas = Canvas(CANVAS_URL, PERSONAL_ACCESS_TOKEN)
course = canvas.get_course(COURSE_ID)
assignment = course.get_assignment(ASSIGNMENT_ID,
include=['overrides'])
assignment_name = assignment.name

# get assignment rubric settings object; retrieve the rubric id
assignment_rubric_settings = assignment.rubric_settings
assignment_rubric_id = assignment_rubric_settings['id']
assignment_rubric_title = assignment_rubric_settings['title']
assignment_rubric_points_possible = assignment_rubric_settings['points_possible']

# get rubric settings
rubric_settings = assignment.rubric
for setting in rubric_settings: print(f"{setting['id']} {setting['description']}")
import pandas as pd

submissions = assignment.get_submissions(include=['rubric_assessment'])

# Create an empty list to store data
data_to_concat = []

for submission in submissions:
    try:
        if hasattr(submission, 'rubric_assessment') and submission.rubric_assessment:
            user_id = submission.user_id
            # Create a dictionary to store data for each rubric item
            rubric_data = {"user_id": user_id}

            # Loop through all rubric items
            for setting in rubric_settings:
                rubric_item_id = setting['id']
                rubric_item_description = setting['description']

                # Get rubric assessment data for the current rubric item
                rubric_item_data = submission.rubric_assessment.get(rubric_item_id, {})
                rating_id_or_score = rubric_item_data.get('points', None)
                comments = rubric_item_data.get('comments', None)

                # Add data for the current rubric item to the dictionary
                rubric_data[f"{rubric_item_description}_points"] = rating_id_or_score
                rubric_data[f"{rubric_item_description}_Comments"] = comments

                # Debugging information
                print(f"Submission {submission.id}, Rubric ID: {rubric_item_id}, Points: {rating_id_or_score}, Comments: {comments}")

            # Append the data for the current submission to the list
            data_to_concat.append(rubric_data)
        else:
            print(f"No rubric assessment for submission {submission.id}")
    except Exception as e:
        print(f"Error processing submission {submission.id}: {e}")
# Create a DataFrame from the list of dictionaries
df = pd.DataFrame(data_to_concat)

# the output file is named after "{assignment_name}_rubric_report.csv"
# you can change the file name format accordingly
download_file_name = f"{assignment_name}_rubric_report.csv"
df.to_csv(download_file_name, index=False)
files.download(download_file_name)
## name 'files' is not defined

Here we extract data from every rubric from every course. We place the API token on a .txt file for security when this runs, python will give several error codes indicating that there are no rubrics. However, that is because several assignments and courses don’t have rubrics. It only captures the ones that do.

!pip install canvasapi
!pip install pandas

from canvasapi import Canvas
import pandas as pd

with open('canvas.txt', 'r') as file:
    PERSONAL_ACCESS_TOKEN = file.readline().strip()

CANVAS_URL = 'https://uwyo.instructure.com/'

canvas = Canvas(CANVAS_URL, PERSONAL_ACCESS_TOKEN)

# Create an empty list to store data
data_to_concat = []

# Function to get instructor name for a given course
def get_instructor_name(course):
    try:
        instructors = course.get_users(enrollment_type='teacher')
        instructor_names = [f"{instructor.short_name}" for instructor in instructors]
        return ', '.join(instructor_names)
    except Exception as e:
        print(f"Error fetching instructor name: {e}")
        return None

# Iterate through all courses
for course in canvas.get_courses():
    try:
        # Get the instructor name for the course
        instructor_name = get_instructor_name(course)

        # Iterate through all assignments in the course
        for assignment in course.get_assignments(include=['rubric']):
            try:
                assignment_name = assignment.name

                # get rubric settings
                rubric_settings = assignment.rubric

                # Iterate through submissions for the assignment
                submissions = assignment.get_submissions(include=['rubric_assessment'])
                
                for submission in submissions:
                    try:
                        if hasattr(submission, 'rubric_assessment') and submission.rubric_assessment:
                            user_id = submission.user_id
                            term = course.enrollment_term_id
                            course_name = course.name
                            
                            # Loop through all rubric items
                            for setting in rubric_settings:
                                rubric_item_id = setting['id']
                                rubric_item_description = setting['description']

                                # Get rubric assessment data for the current rubric item
                                rubric_item_data = submission.rubric_assessment.get(rubric_item_id, {})
                                rating_id_or_score = rubric_item_data.get('points', None)

                                # Add data for the current rubric item to the list
                                data_to_concat.append({
                                    "user_id": user_id,
                                     "instructor_name": instructor_name,
                                    "assignment_name": assignment_name,
                                    "course_name": course_name,
                                    "rubric_item_description": rubric_item_description,
                                    "score": rating_id_or_score
                                   
                                })
                        else:
                            print(f"No rubric assessment for submission {submission.id}")
                    except Exception as e:
                        print(f"Error processing submission {submission.id}: {e}")
            except Exception as e:
                print(f"Error processing assignment {assignment.id}: {e}")
    except Exception as e:
        print(f"Error processing course {course.id}: {e}")

# Create a DataFrame from the list of dictionaries
df = pd.DataFrame(data_to_concat)

# the output file is named after "rubric_report.csv"
# you can change the file name format accordingly
download_file_name = "rubric_report.csv"
df.to_csv(download_file_name, index=False)
print(f"Rubric data exported to {download_file_name}")

A little bit of tidy work

rubricdata<- read.csv("rubric_report.csv")

rubricdata<- rubricdata%>%
  group_by(instructor_name, assignment_name, course_name, rubric_item_description)%>%
  mutate(total_possible = max(score, na.rm = TRUE))%>%
  mutate(percent = score/total_possible)
  
write.csv(rubricdata, "rubric.csv")

Time to program an application to use to analyze the data. It includes login credential requirements.

library(tidyverse)
library(rpivotTable)
library(dplyr)
library(readr)
library(rvest)
library(shiny)
library(shinydashboard)
library(writexl)
library(htmlwidgets)
library(shinyjs)
library(clipr)
library(shinymanager)
library(shinyWidgets)
library(ggplot2)
library(plotly)
library(readxl)



j <- "
function filter(node){
  return (node.tagName !== 'i');
}
function exportPlot(filename){
  var plot = document.getElementsByClassName('pvtRendererArea');
  domtoimage.toPng(plot[0], {filter: filter, bgcolor: 'white'})
    .then(function (dataUrl) {
      var link = document.createElement('a');
      link.download = filename;
      link.href = dataUrl;
      link.click();
    });
}
Shiny.addCustomMessageHandler('export', exportPlot);"

gradescleaned<- read.csv("gradescleaned.csv")%>%
  select(term, course, assignment_name, assignment_id, score, max_score, percent, grade, numbergrade, section, campus, concatenate, Standard_Overall)

rubriccleaned<- readxl::read_excel("rubric.XLSX")%>%
  select(-percent)

rubriccleaned$score<- as.numeric(rubriccleaned$score)
rubriccleaned$total_possible<- as.numeric(rubriccleaned$total_possible)

rubriccleaned<- rename(rubriccleaned, Possible = total_possible)

rubriccleaned<-  gather(rubriccleaned, Score_Type, Score, score:Possible, factor_key=TRUE)

rubriccleaned<- rename(rubriccleaned, Type = Score_Type)

rubriccleaned<- rename(rubriccleaned, Criteria = rubric_item_description)

rubriccleaned$Score2<- as.character(rubriccleaned$Score)

rubriccleaned<- na.omit(rubriccleaned)

credentials <- data.frame(
  user = c("mark", "paul"), 
  password = c("mark", "paul"), 
  start = c("2023-02-01"), 
  expire = c(NA, "2025-12-31"),
  admin = c(TRUE, FALSE),
  comment = "Simple and secure authentification mechanism 
  for single 'Shiny' applications.",
  stringsAsFactors = FALSE
)


ui<-   secure_app(
  dashboardPage(skin = "black", 
                
                dashboardHeader(title ="Outcomes"), 
                
                dashboardSidebar(
                  sidebarMenu(
                    menuItem("Introduction", tabName = "intro", icon = icon("user")), 
                    menuItem("Grades", tabName = "grades", icon = icon("user")), 
                    menuItem("Outcomes", tabName = "outcomes", icon = icon("user")))), 
                
                dashboardBody(
                  tabItems(
                    tabItem(tabName = "intro",
                            fluidPage(setBackgroundImage(src = "image.jpg", shinydashboard = TRUE),
                                      h1("Grades and Outcomes Dashboard",
                                         style = "position: absolute; bottom: 0;right:3;", style = "color: white" ))),
                    tabItem(tabName = "grades",
                            fluidPage(
                              tags$head(
                                tags$script(src = "dom-to-image.min.js"),
                                tags$script(HTML(j))),
                              radioButtons(inputId = "format", 
                                           label = tags$p("Enter the format to download tables or export graphs as .png files", style = "color:blue"),
                                           choices = c( "csv", "excel"), inline = FALSE, selected = "csv"),
                              downloadButton("download_pivot"),
                              actionButton("export", "Export Graph"),
                              fluidRow(br(),
                                       div(style = 'overflow-x: scroll', rpivotTableOutput("pivot")),
                                       br()))
                    ),
                    tabItem(tabName = "outcomes", 
                            h3("Canvas Rubric Data", align = "center"), 
                            fluidRow(
                              box(pickerInput("courseInput",
                                              label = tags$p("Select or type one or more courses"), 
                                              choices = sort(unique(rubriccleaned$course_name)), 
                                              options = list("actions-box" = TRUE),
                                              selected = "Fall 2020 Play Therapy (CNSL-5340-50)",
                                              multiple = TRUE),
                                  uiOutput("assignInput"))), 
                            fluidRow(
                              box(plotlyOutput("plot", height = 500), width = 1000))))))
)




server<-  function (input, output, session) { 
  
  res_auth <- secure_server(
    check_credentials = check_credentials(credentials)
  )
  
  
  df0  <-  eventReactive(input$courseInput, {
    rubriccleaned %>% filter(course_name %in% input$courseInput)
  })
  output$assignInput  <-  renderUI({
    selectInput("assignInput", label = "Select and Assignment",
                choices = sort(unique(df0()$assignment_name)),
                selected = "Play Therapy Theories Research Paper (Final Research Paper)",
                multiple = TRUE)
  })
  
  df1  <-  eventReactive(input$assignInput, {
    df0() %>% filter(assignment_name %in% input$assignInput)
  })   
  
  
  output$pivot <- renderRpivotTable({
    rpivotTable(gradescleaned, rows = c("assignment_name", "Standard_Overall", "term"),
                aggregatorName = "Average",
                vals = "numbergrade",
                sorters = "function(attr) 
                { var sortAs = $.pivotUtilities.sortAs; if (attr == \"term\") 
                { return sortAs([\"19/FA\", \"20/SP\", \"20/SU\", \"20/FA\",
                \"21/SP\", \"21/SU\", \"21/FA\", \"22/SP\", \"22/Su\"]); } }",
                onRefresh = htmlwidgets::JS(
                  "function(config) {
                            Shiny.onInputChange('pivot', document.getElementById
                            ('pivot').innerHTML); 
                        }"), rendererOptions = list(
                          c3 = list(legend = list(show = FALSE), # hide legend
                                    data = list(labels = TRUE),  # label the data
                                    size = list(width = "600",   # control the size
                                                height = "500"),
                                    color = "black")))
  })
  
  
  pivot_tbl <- eventReactive(input$pivot, {
    tryCatch({
      input$pivot %>%
        read_html %>%
        html_table(fill = TRUE) %>%
        .[[2]]
    }, error = function(e) {
      return()
    })
  })
  
  # allow the user to download once the pivot_tbl object is available
  observe({
    if (is.data.frame(pivot_tbl()) && nrow(pivot_tbl()) > 0) {
      shinyjs::enable("download_pivot")
      shinyjs::enable("export")
    } else {
      shinyjs::disable("download_pivot")
      shinyjs::disable("export")
    }
  })
  
  # using shiny's download handler to get the data output
  output$download_pivot <- downloadHandler(
    filename = function() {
      if (input$format == "csv") {
        "pivot.csv"
      } else if (input$format == "excel") {
        "pivot.xlsx"
      }
    },
    content = function(file) {
      if (input$format == "csv") {
        write_csv(pivot_tbl(), path = file)
      } else if (input$format == "excel") {
        writexl::write_xlsx(pivot_tbl(), path = file)
      } 
    }
  )
  output$auth_output <- renderPrint({
    reactiveValuesToList(res_auth)
  })
  
  observeEvent(input$"export", {
    session$sendCustomMessage("export", "plot.png")
  })
  
  
  output$plot  <-  renderPlotly({
 
 df1_summary <- df1() %>%
  group_by(Criteria, Type) %>%
  summarise(mean_Score = mean(Score),
            user_count = n_distinct(user_id))

# Check the structure of df1_summary
str(df1_summary)

# Plotting code
demo <- ggplot(df1_summary, aes(x = Criteria, y = mean_Score, fill = Type, text = paste("Type: ", Type, "\nMean Score: ", round(mean_Score, 2), "\nUser Count: ", user_count))) +
  geom_bar(stat = "identity", position = "dodge") +
  geom_text(aes(label = sprintf("%.2f", mean_Score), y = mean_Score - 0.5), color = "white", position = position_dodge(width = 0.9), size = 3) +
  ggtitle("Rubrics") +
  scale_fill_brewer(palette = "Dark2") +
  coord_flip() +
  theme(axis.text.x = element_text(angle = 45)) +
  labs(x = "Points", y = "Rubric Criteria")

# Convert ggplot to plotly
ggplotly(demo, tooltip = "text")
    
    
  }) 
  
}

shinyApp(ui = ui, server = server)