Fun with Canvas APIs and content

maguire
Community Champion
10
8949

I have been encouraged to make the contents of my sandbox more visible, it can be found at https://kth.instructure.com/courses/11  One of my goals has been to be able to use software to interact with Canvas, rather than being limited to cut and paste with RCE. Another of my goals has been to use programs to move content out of various systems that we are currently using and into Canvas. Another goal has been to move content in the other direction to other external systems.

My sandbox contains a lot of examples of things that I have been experimenting with, including computing readability statistics for the page of a course, adding citations to pages (via Zotero) using an RCE buttom to call on an LTI tool provider [a work in progress], and today's exporting of a spreadsheet of all assignments for a course (using Python's Pandas). I'm eager for feedback and opinions on this material.

Tags (1)
10 Comments
sonya_corcoran1
Community Contributor

This is BRILLIANT Gerald, thanks so much for sharing.

I'm interested in RCE LTI tools too - would love to keep updated on your progress.

Also, can you get the Code Embed tool to work? I'm getting a 500 error on https://code-embed-lti.herokuapp.com/save_editor

Cheers

Sonya

maguire
Community Champion
Author

I do not use herokuapp.com, but rather my own Sinata script running locally:

# -*- coding: utf-8 -*-
# original youtube_lti.rb file written by Brian Whitmer
#
# changes and comments by G. Q. Maguire Jr. - in July 2016
# in his effort to learn about LTI
#
begin
  require 'rubygems'
rescue LoadError
  puts "You must install rubygems to run this example"
  raise
end
begin
  require 'bundler/setup'
rescue LoadError
  puts "to set up this example, run these commands:"
  puts "  gem install bundler"
  puts "  bundle install"
  raise
end
require 'sinatra'
require 'dm-core'
require 'dm-migrations'
require 'socket'
# The code regarding SSL is based upon richard_bw's posting: http://stackoverflow.com/questions/3696558/how-to-make-sinatra-work-over-https-ssl
# create the self-signed certificates as described at http://www.eclectica.ca/howto/ssl-cert-howto.php
#
require 'sinatra/base'
require 'webrick'
require 'webrick/https'
require 'openssl'
require 'json'
require 'net/http'
require 'net/https'
require 'uri'
# https://github.com/dmendel/bindata
require 'bindata'
#CERT_PATH = '/opt/myCA/server/'
CERT_PATH = '/u2/home/maguire/Working/Canvas/LTI-experiments/'
# SSL fields and constants described at http://ruby-doc.org/stdlib-2.0.0/libdoc/webrick/rdoc/WEBrick/Config.html
webrick_options = {
   :Port  => 14921,
   :Logger  => WEBrick::Log::new($stderr, WEBrick::Log::DEBUG),
   :DocumentRoot  => "public",
   :SSLEnable  => true,
   :SSLVerifyClient  => OpenSSL::SSL::VERIFY_NONE,
   :SSLCertificate  => OpenSSL::X509::Certificate.newFile.open(File.join(CERT_PATH, "cert.pem")).read),
   :SSLPrivateKey  => OpenSSL::PKey::RSA.newFile.open(File.join(CERT_PATH, "key.pem")).read),
# :SSLCertName => [ [ "CN",WEBrick::Utils::getservername ] ]
   :SSLCertName  => [ [ "CN", "127.0.0.1" ] ]
}
puts "getservername: "
puts WEBrick::Utils::getservername
puts webrick_options[:SSLCertName]
def new_document_settings(course, x)
# setup the json values to be stored for this course
# zotero_settings_string contains the Zotero information
  zotero_settings_string = zotero_settings.to_json
  #File.write('ZZZZZZ-Zotero-settings.json', zotero_settings_string)
end
class MyServer  < Sinatra::Base
  get '/zotero_settings' do
  puts "called get zotero_settings"
   @zotero_settings_string = File.read('ZZZZZZ-Zotero-settings.json')
   @zotero_settings = JSON.parse(@zotero_settings_string)
  puts "get zotero_settings: #{@zotero_settings.to_s}"
   @zotero_key = @zotero_settings['zotero']['key']
   @zotero_userid = @zotero_settings['zotero']['userid']
  erb :get_zotero_settings
  end
  get '/document_settings/:course_id' do
  puts "called get document_settings"
  puts "course_id is #{params['course_id']}"
   @canvas_course_number = params['course_id']
   @zotero_settings_string = File.read('ZZZZZZ-Zotero-settings.json')
   @zotero_settings = JSON.parse(@zotero_settings_string)
   @data_version=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['data-version']
   @zotero_version=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['zotero-version']
   @style_id=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['style id']
   @hasBibliography=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['hasBibliography']
   @bibliographyStyleHasBeenSet=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['bibliographyStyleHasBeenSet']
   @fieldType=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['fieldType']
   @storeReferences=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['storeReferences']
   @automaticJournalAbbreviations=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['automaticJournalAbbreviations']
   @noteType=@zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['noteType']
   # "session id": "HtxmZxzR",
  erb :get_document_settings
  end
  get '/document_settings' do
  puts "called get_document_preferences without a course number"
  erb :set_course_number
  end
  get '/ombed/*' do
  {
"foo" => "bar",
"baz" => 1
  }
  end
  get '/' do
   "Hellow, world! Happy Wednesday!"
  end  
#end
### end of part 1 of SSL code
#
# Sinatra wants to set x-frame-options by default, disable it
disable :protection
# Enable sessions so we can remember the launch info between http requests, as
# the user does what ever action they are going to do
enable :sessions
# by default the TCP port number 4567 is used, set it to something else for learning about LTI
#set :port, 14921
# Note that any GET requests for files are automatically served by Sinatra from the "public" sub-drectory or the directory where this program is executing from
# this behavior can be changed by changing the value of :public_folder
post "/zotero_settings" do
  puts "called zotero_settings"
  puts "params: #{params}"
   @zotero_settings_string = File.read('ZZZZZZ-Zotero-settings.json')
   @zotero_settings = JSON.parse(@zotero_settings_string)
   @zotero_settings['zotero']['key']=params['zotero_key']
   @zotero_settings['zotero']['userid']=params['zotero_userid']
   @zotero_settings_string= @zotero_settings.to_json
   #File.write('ZZZZZZ-Zotero-settings.json', @zotero_settings_string)
end
post "/set_coursenumber" do
  puts "called set_coursenumber"
  puts "params: #{params}"
   if params[:canvas_course_id]
  redirect('/document_settings/'+params[:canvas_course_id].to_s)
   else
  redirect('/document_settings')
   end
end
post "/document_settings" do
  puts "called document_settings"
  puts "params: #{params}"
end
post "/set_document_preferences" do
  puts "called set_document_preferences"
  puts "params: #{params}"
   @canvas_course_number = params['canvas_course_number']
   @zotero_settings_string = File.read('ZZZZZZ-Zotero-settings.json')
   @zotero_settings = JSON.parse(@zotero_settings_string)
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['data-version']=params['data_version']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['zotero-version']=params['zotero_version']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['style id']=params['style_id']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['hasBibliography']=params['hasBibliography']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['bibliographyStyleHasBeenSet']=params['bibliographyStyleHasBeenSet']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['fieldType']=params['fieldType']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['storeReferences']=params['storeReferences']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['automaticJournalAbbreviations']=params['automaticJournalAbbreviations']
   @zotero_settings[@canvas_course_number]['Zotero_Document_settings']['prefs']['noteType']=params['noteType']
  puts "about to set document preferences to #{@zotero_settings.to_s}"
   @zotero_settings_string = @zotero_settings.to_json
  puts "@zotero_settings_string: #{@zotero_settings_string}"
   #File.write('ZZZZZZ-Zotero-settings.json', @zotero_settings_string)
end
# Handle POST requests to the endpoint "/lti_launch"
post "/lti_launch" do
   # NOTE: This process isn't checking for correct signatures, anyone that sends a
   # POST request to /lti_launch with the two required parameters will be able
   # to set a placement with a follow-up request to /set_video. I'll cover adding
   # that in another example. There are some great libraries that make it pretty
   # easy, but that wasn't the point of this first example.
  puts "params to /lti_launch"
  puts params
  #
   # get the course ID either from a custom field or from the return URL
  course_id=params['custom_course_id']
   if course_id == nil
  str1=params['launch_presentation_return_url']
  course_id=str1.split('/')[4]
   end
  puts "course_id: #{course_id}"
  zotero_settings_string = File.read('ZZZZZZ-Zotero-settings.json')
  zotero_settings = JSON.parse(zotero_settings_string)
  puts "zotero_settings: #{zotero_settings}"
  zotero_user_id=zotero_settings['zotero']['userid']
  zotero_users_key=zotero_settings['zotero']['key']
  puts "zotero user: #{zotero_user_id} has key: #{zotero_users_key}"
   # sent a request message to Zotero in the browser via TCP port 23116
  socket_to_zotero = TCPSocket.new '127.0.0.1', 23116
   # cmd_len = new Uint8Array(8)
   #cmd_len[0]=0x00
   #cmd_len[1]=0x00
   #cmd_len[2]=0x00
   #cmd_len[3]=0x00
   #cmd_len[4]=0x00
   #cmd_len[5]=0x00
   #cmd_len[6]=0x00
   #cmd_len[7]=0x0d
   #socket_to_zotero.write(cmd_len)
   #count=socket_to_zotero.write('
"addCitation"')
  command = '"addCitation"'
  message = ZoteroMsg.new :s=>0, :len=>command.length, :msg=> command
   #command_response=message.write(socket_to_zotero)
  count=socket_to_zotero.write(message.to_binary_s)
  puts "counts: #{count}"
   # Zotero in the browser should send ["Application_getActiveDocument",[3]]
   # according to zotero-libreoffice-integration-master/components/zoteroOpenOfficeIntegration.js
   # the form of this message is "Application_getActiveDocument", [API_VERSION]
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
   API_Version=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg API_Version: #{API_Version}"
   # client sends 0x00000001 and 0x00000005 and the string [3,1]
   # this seems to be of the form [API_VERSION, documentID]
   #
   # if documentID=1, then the user will be prompted to set their Document preferences
  documentID=2
  client_msg1 = '['+API_Version.to_s+',' + documentID.to_s+']'
   #client_msg1 = '[3,1]'
  message1 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg1.length, :msg=> client_msg1
  count=socket_to_zotero.write(message1.to_binary_s)
   # server should reply with 0x00000002 and 0x00000020) and the string ["Document_getDocumentData",[1]]
   # the form of this document command is "Document_"+methodStable, [this._documentID].concat(Array.prototype.slice.call(arguments)))
   # note that at this point we have the page for "Document Preferences"
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 1 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  documentID_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg documentID_response: #{documentID_response}"
  document_settings=zotero_settings[course_id]['Zotero_Document_settings']
  puts "document_settings: #{document_settings}"
   # if there are no document settings for this course, then set the document_ID to 1 so that the user will be prompted to set their document preferences
   if document_settings == nil
  documentID = 1
   end
   # client sends 0x00000002 and 0x00000002) and the string ""
   if documentID == 1
  client_msg2 = '""'
   else
# client_msg2 = '"<data data-version=\"3\" zotero-version=\"4.0.29.10\"><session id=\"HtxmZxzR\"/><style id=\"http://www.ict.kth.se/courses/II2202/IEEElike-with-access.csl\" hasBibliography=\"1\" bibliographyStyleHasBeenSet=\"0\"/><prefs><pref name=\"fieldType\" value=\"ReferenceMark\"/><pref name=\"storeReferences\" value=\"true\"/><pref name=\"automaticJournalAbbreviations\" value=\"\"/><pref name=\"noteType\" value=\"\"/></prefs></data>"'
  client_msg2 ='"<data data-version=\"'+document_settings['data-version']+\
   '\" zotero-version=\"'+document_settings['zotero-version']+'\"><session id=\"'+\
  document_settings['session id']+'\"/><style id=\"'+\
  document_settings['style id']+'\" hasBibliography=\"'+\
  document_settings['hasBibliography']+'\" bibliographyStyleHasBeenSet=\"'+\
  document_settings['bibliographyStyleHasBeenSet']+'\"/><prefs><pref name=\"fieldType\" value=\"' +\
  document_settings['prefs']['fieldType']+'\"/><pref name=\"storeReferences\" value=\"'+\
  document_settings['prefs']['storeReferences']+'\"/><pref name=\"automaticJournalAbbreviations\" value=\"'+\
  document_settings['prefs']['automaticJournalAbbreviations']+'\"/><pref name=\"noteType\" value=\"'+\
  document_settings['prefs']['noteType']+'\"/></prefs></data>"'
  puts "document configuration: #{client_msg2}"
   end
   #
  message2 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg2.length, :msg=> client_msg2
  count=socket_to_zotero.write(message2.to_binary_s)
   # the following are only needed when no document document preferences are set or when they are set and the system sends a
   # new Document_setDocumentData because the session ID has changed.
   # server should reply with 0x00000003 and 0x000001cc response string giving the document settings
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 3 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
   #if zotero_response.msg[0] == "Document_setDocumentData"
   # new_document_settings(zotero_response.msg[1][1])
   # client sends 0x00000003 and 0x00000004) and the string null
  client_msg3 = 'null'
  message3 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg3.length, :msg=> client_msg3
  count=socket_to_zotero.write(message3.to_binary_s)
   # server should reply with 0x00000004 and 0x0000002f) and the string ["Document_canInsertField",[1,"ReferenceMark"]]
   # of the form: return Comm.sendCommand("Document_"+methodStable,
   # [this._documentID].concat(Array.prototype.slice.call(arguments)));
   #
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 4 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  referencemark_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg referencemark_response: #{referencemark_response}"
   # client sends 0x00000004 and 0x00000004 and the string true
  client_msg4 = 'true'
  message4 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg4.length, :msg=> client_msg4
  count=socket_to_zotero.write(message4.to_binary_s)
   # server should reply with 0x00000005 and 0x0000002e and the string ["Document_cursorInField",[1,"ReferenceMark"]]
   # var retVal = Comm.sendCommand("Document_cursorInField", [this._documentID, fieldType]);
   # if(retVal === null) return null;
   # return new Field(this._documentID, retVal[0], retVal[1], retVal[2]);
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 5 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  referencemark_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg referencemark_response: #{referencemark_response}"
   # client sends 0x00000005 and 0x00000004 and the string null
  client_msg5 = 'null'
  message5 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg5.length, :msg=> client_msg5
  count=socket_to_zotero.write(message5.to_binary_s)
   # server should reply with 0x00000006 and 0x0000002e and the string "Document_insertField",[1,"ReferenceMark",0]]
   # var retVal = Comm.sendCommand("Document_insertField", [this._documentID, fieldType, noteType]);
   # return new Field(this._documentID, retVal[0], retVal[1], retVal[2]);
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 6 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  referencemark_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} referencemark_response: #{referencemark_response}"
   # client sends 0x00000006 and 0x00000008) and the string [0,"",0]
  client_msg6 = '[0,"",0]'
  message6 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg6.length, :msg=> client_msg6
  count=socket_to_zotero.write(message6.to_binary_s)
   # server should reply with 0x00000007 and 0x0000001e and the string ["Field_setCode",[1,0,"TEMP"]]
   # of the form: Comm.sendCommand("Field_setCode", [this._documentID, this._index, code]);
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 7 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  referencemark_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} referencemark_response: #{referencemark_response}"
   # client sends 0x00000007 and 0x00000004) and the string null
  client_msg7 = 'null'
  message7 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg7.length, :msg=> client_msg7
  count=socket_to_zotero.write(message7.to_binary_s)
   # server should reply with 0x00000008 and 0x0000002a and the string ["Document_getFields",[1,"ReferenceMark"]]
   # of the form: Comm.sendCommandAsync("Document_getFields", [this._documentID, fieldType],
   # function(retVal) {observer.observe(new FieldEnumerator(documentID, retVal[0], retVal[1], retVal[2]), "fields-available", null);},
   #
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 8 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  referencemark_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} referencemark_response: #{referencemark_response}"
   # client sends 0x00000008 and 0x00000012) and the string [[0],["TEMP"],[0]]
   #
  client_msg8 = '[[0],["TEMP"],[0]]'
  message8 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg8.length, :msg=> client_msg8
  count=socket_to_zotero.write(message8.to_binary_s)
   # server should reply with 0x00000009 and 0x00000023 and the string ["Field_setText",[1,0,"[1]",false]]
   # Comm.sendCommand("Field_"+methodStable, [this._documentID, this._index].concat(Array.prototype.slice.call(arguments)));
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 9 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  setText_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} setText_response: #{setText_response}"
   # client sends 0x00000009 and 0x00000004 and the string null
  client_msg9 = 'null'
  message9 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg9.length, :msg=> client_msg9
  count=socket_to_zotero.write(message9.to_binary_s)
   # server should reply with 0x0000000a and 0x00000017 and the string ["Field_getText",[1,0]]
   # Comm.sendCommand("Field_"+methodStable, [this._documentID, this._index].concat(Array.prototype.slice.call(arguments)));
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 10 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  getText_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} getText_response: #{getText_response}"
   # client sends 0x0000000a and 0x00000005 and the string "[1]"
  client_msg10 = '"[1]"'
  message10 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg10.length, :msg=> client_msg10
  count=socket_to_zotero.write(message10.to_binary_s)
   # server should reply with 0x0000000b and 0x000003eb and the string with a citation starting ["Field_setCode",[1,0,"ITEM CSL_CITATION {\"citationID\":\"12c6kebdhk\",\"properties\":{\"formattedCitation\":\"[1]\",\"plainCitation\":\"[1]\"},\"citationItems\":[{\"id\":21413,\"uris\":[\"http://zotero.org/users/683389/items/EHEFWZ96\"],\"uri\":[\"http://zotero.org/users/683389/items/EHEFWZ96\"],\"itemData\":{\"id\":21413,\"type\":\"book\",\"title\":\"Radiation protection in the radiologic and health sciences\",\"publisher\":\"Lea & Febiger\",\"publisher-place\":\"Philadelphia\",\"number-of-pages\":\"218\",\"source\":\"Library of Congress ISBN\",\"event-place\":\"Philadelphia\",\"ISBN\":\"0-8121-0657-1\",\"call-number\":\"RC78.3 .N69 1979\",\"author\":[{\"family\":\"Noz\",\"given\":\"Marilyn E.\"}],\"issued\":{\"date-parts\":[[\"1979\"]]}}}],\"schema\":\"https://github.com/citation-style-language/schema/raw/master/csl-citation.json\"}"]]
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 11 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
   # save the citation that is returned
  zotero_response_msg_string=zotero_response.msg
   # puts "zotero_response_msg_string length #{zotero_response_msg_string.length} is #{zotero_response_msg_string}"
   # compute a URL for the Zotero API add the user's key zotero_users_key
  zotero_response_msg_JSON=JSON.parse(zotero_response_msg_string)
   if zotero_response_msg_JSON[0]=="Field_setCode"
  field=zotero_response_msg_JSON[1][2]
   if field.index("ITEM CSL_CITATION") == 0
  citation_info=JSON.parse(field[(("ITEM CSL_CITATION".length)+1)..-1])
  puts "citation_info: #{citation_info.to_s}"
   # process first citation item and take the first uri from this vector
  citation_uri=citation_info['citationItems'][0]['uri'][0]
  puts "citation_uri: #{citation_uri}"
   if citation_uri.length > 0
  puts "citation_uri.length: #{citation_uri.length}"
   # note that you have to use the %q{} to prevent the slashes and period from being considered part of a regular expression
   # citation_uri.sub(%q{http://zotero.org/}, 'http://api.zotero.org/')
  citation_uri='http://api.zotero.org/' << citation_uri["http://zotero.org/".length..-1]
  puts "citation_uri: #{citation_uri}"
  citation_uri << '?format=bib&key=' << zotero_users_key
  puts "citation_uri: #{citation_uri}"
   end
   end
   end
  string_to_insert=zotero_response.msg
# client sends 0x0000000b and 0x00000004 and the string null
  client_msg11 = 'null'
  message11 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg11.length, :msg=> client_msg11
  count=socket_to_zotero.write(message11.to_binary_s)
   # server should reply with 0x0000000c ["Document_activate",[1]]
   # Comm.sendCommand("Document_"+methodStable, [this._documentID].concat(Array.prototype.slice.call(arguments)));
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 12 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  doc_activate_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} doc_activate_response: #{doc_activate_response}"
   # client sends 0x0000000b and 0x00000004 and the string null
  client_msg12 = 'null'
  message12 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg12.length, :msg=> client_msg12
  count=socket_to_zotero.write(message12.to_binary_s)
   # server should reply with 0x0000000c ["Document_complete",[5]]
   # of the form Comm.sendCommand("Document_"+methodStable, [this._documentID].concat(Array.prototype.slice.call(arguments)));
  zotero_response=ZoteroMsg.read(socket_to_zotero)
  puts "ZoteroMsg 13 #{zotero_response.s} is #{zotero_response.len} x #{zotero_response.msg}"
  doc_complete_response=JSON.parse(zotero_response.msg)[1][0]
  puts "ZoteroMsg #{JSON.parse(zotero_response.msg)[0]} doc_complete_response: #{doc_complete_response}"
   # client sends 0x0000000c and 0x00000004 and the string null
  client_msg13 = 'null'
  message13 = ZoteroMsg.new :s=>zotero_response.s, :len=>client_msg13.length, :msg=> client_msg13
  count=socket_to_zotero.write(message13.to_binary_s)
   ### completed the client's command
   #while line = socket_to_zotero.gets
   # puts line.chop
   #end
  socket_to_zotero.close
   #string_to_insert="<p>Some text to insert <a href=\" http://imscatalog.org/\">catalog of certified products</a> junkk junk and more junk</p>"
   # puts "string_to_insert: "
   # puts string_to_insert
   # puts "params['ext_content_return_url']: "
   # puts params['ext_content_return_url']
   #
   # The code below uses the information from https://canvas.instructure.com/doc/api/file.editor_button_tools.html to determine what can be passed:
   #
   # to insert a URL
  new_url=params['ext_content_return_url']+"?return_type=url&url="+CGI::escape(citation_uri)+"&title="+CGI::escape("inserted text")+"&text="+CGI::escape(string_to_insert)
   #
   # to insert a IMG
   #new_url=params['ext_content_return_url']+"?return_type=image_url&url="+CGI::escape("https://localhost:14921/emacs_16.png")+"&alt="+CGI::escape("inserted_text")+"&height=32&width=32"
   #
   # to insert a iframe
   #new_url=params['ext_content_return_url']+"?return_type=iframe&url="+CGI::escape("http://localhost:14921/text_to_return.html")+"&title="+CGI::escape("inserted text")+"&height=32&width=32"
   #new_url=params['ext_content_return_url']+"?return_type=iframe&url="+CGI::escape("http://localhost:14921/text_to_return.html")+"&title="+CGI::escape("inserted text")
  puts new_url
  redirect to (new_url)
end
# /resource_selection?editor=1&selection=Some%20text%20to%20be%20replaced.
post "/resource_selection/*" do
  puts "params to POST /resource_selection"
  puts params
  {"success" => true}.to_json
end
# /resource_selection?editor=1&selection=Some%20text%20to%20be%20replaced.
get "/resource_selection/*" do
  puts "params to GET /resource_selection"
  puts params
  {"success" => true}.to_json
end
# Handle POST requests to the endpoint "/set_video"
post "/set_video" do
  if session["can_set_" + params['placement_id']]
   Placement.create(:placement_id => params['placement_id'], :video_id => params['video_id'])
   return '{"success": true}'
  else
   return '{"success": false}'
  end
end
class ZoteroMsg < BinData::Record
# endian :little
  endian :big
  uint32 :s
  uint32 :len
  string :msg, :read_length => :len
end
# Data model to remember placements
class Placement
  include DataMapper::Resource
  property :id, Serial
  property :placement_id, String, :length => 1024
  property :video_id, String
  property :content, Text   # added for editor functionality
  property :editor_settings, String, :length => 1024   # added for editor functionality
  property :user_id, String, :length => 1024   # added for editor functionality
end
env = ENV['RACK_ENV'] || settings.environment
DataMapper.setup(:default, (ENV["DATABASE_URL"] || "sqlite3:///#{Dir.pwd}/#{env}.sqlite3"))
DataMapper.auto_upgrade!
#### part 2 of SSL code ####
end   #end of class MyServer
Rack::Handler::WEBrick.run MyServer, webrick_options
#Rack::Server.start webrick_options
###### End of code for SSL
chun_li
Community Contributor

Hi Gerald,

I like your idea.

I tried to open your sandbox but it requires login. Is there any way I can access it without login?

Thanks,

Chun

Stef_retired
Instructure Alumni
Instructure Alumni

 @chun_li ​, I can access the course on my PC via Firefox and on an iPad through SafarI without having to log in. Did you try clearing  your cache or accessing it through a different browser? Or an incognito window?

chun_li
Community Contributor

Hi Stefanie, I can open the course now, don't know why I couldn't. But thanks!

maguire
Community Champion
Author

Due to the evolution of our integration of students into Canvas, existing courses have a mixture of different name orders. Unfortunately, this makes it difficult when interacting with other systems (such as the national grade registry). Additionally, it seemed to it was unfortunate that one could not easily group the students in the gradebook by project groups. Therefore it was time to do something about this.

I combine the following steps to produce the results that I wanted:

1. extract a list of students from a course (based upon course_id)

2. extract the sis_user_id column (which gives the local KTHIDs that can be used with LDAP to find the student's first and last names) and using a program running on one of the university computers convert the file of IDs (one per line) to a CSV file with KTHID, Last_name, First_name.

3. augment the spreadsheet produced in step 1 with Last_name and First_name

4. augment the previously augmented spreadsheet with group membership information (in this case the project groups are name Project Groups P1P2 1 .. n)

5. now insert the desired new columns as custom columns in the gradebook

Steps 1..3 can be see at Getting last_name and first_name from KTHIDs (used as SIS user IDs): Chip sandbox

while steps 4 and 5 can be see at Adding group numbers to list of students: Chip sandbox .

The python programs are available from the above pages. These programs make use of python Pandas for dealing with the spreadsheets, making the process quite simply.

Note that the program for inserting new columns into the gradebook can be used with just about any data you want as long as one of the columns (named 'id') contains the user id of a student. Currently, the program only deals with spreadsheet cells of types object (interpreted as strings), int64, and float64. Note that floats that could be represented as integers are converted so that you get the integer, rather than xxxx.0 - which looks rather strange if these are group numbers.

kmeeusen
Community Champion

Hi  @maguire  

Thank you for starting this awesome blog!

We have created, or rather  @scottdennis ‌ created, a classroom in Resources.Instructure where folks share coding tips, tricks, techniques and snippets of codes. We call it Canvas Hacks Demo Course. We would love to have you and your expertise join us!  DM me your email address if you would like Teacher access (I think that would be best for your skill level), or follow this link for student access: https://resources.instructure.com/enroll/8R9H7B

If you do not already have an account at this instance, you will be prompted to create one.

Hope you come and play in our sandbox!

KLM

chrisw
Instructure
Instructure

Hi Chip,

This was a wonderful find as I'm preparing to share the community with our friends in Norway!  I will be pointing the Developers to this post, to show some of the fun things you can do with the Canvas API. 

Thank you and I look forward to hopefully seeing you at CanvasCon Scandinavia next month!

maguire
Community Champion
Author

Recently I have been working at inserting questions into Canvas using the API. I found it very useful to have some simple test programs that could insert specific types of questions. Anyone who is interested can find several of these programs at Directly inserting different types of quizzes via the API: Chip sandbox 

maguire
Community Champion
Author

Adding "calculated_question" quizzes - see the https://kth.instructure.com/courses/11/pages/adding-calculated-question-quizzes-via-api  . Note that one of the surprises is that the formula itself has to be put in a way that is not clear from the Model or the document (or even from the Canvas source code).