cancel
Showing results for 
Search instead for 
Did you mean: 
maguire
Adventurer II

Problems with trying to return ContentItems to the Rich Context Editor

I have written a small simple chunk of ruby sinatra code to be able to add a button to the Rich Content Editor

(1) the img insert does not include the alt text:

<img style="width: 32px; height: 32px;" src="https://localhost:14921/emacs_16.png" alt="" /></p>

The code is as follows:

# 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

# {"oauth_consumer_key"=>"consumerkey",

#   "oauth_signature_method"=>"HMAC-SHA1",

#   "oauth_timestamp"=>"1469633132", "oauth_nonce"=>"WBo7X2jy4RbwHOrpoMjktPcEflvZVBcVdvkLw1iJZE", "oauth_version"=>"1.0",

#   "context_id"=>"08d76eee8fbed3381e05c688c1f630b7d84cf06c", "context_label"=>"Chip", "context_title"=>"Chip sandbox", "custom_canvas_enrollment_state"=>"active", "custom_fooo"=>"125690",

#   "ext_content_intended_use"=>"embed", "ext_content_return_types"=>"oembed,lti_launch_url,url,image_url,iframe",

#   "ext_content_return_url"=>"https://kth.instructure.com/courses/11/external_content/success/external_tool_dialog",

#   "ext_roles"=>"urn:lti:instrole:ims/lis/Instructor,urn:lti:role:ims/lis/Instructor,urn:lti:sysrole:ims/lis/User",

#   "launch_presentation_document_target"=>"iframe", "launch_presentation_height"=>"500", "launch_presentation_locale"=>"en-GB",

#   "launch_presentation_return_url"=>"https://kth.instructure.com/courses/11/external_content/success/external_tool_dialog",

#   "launch_presentation_width"=>"500", "lti_message_type"=>"basic-lti-launch-request", "lti_version"=>"LTI-1p0",

#   "oauth_callback"=>"about:blank", "resource_link_id"=>"08d76eee8fbed3381e05c688c1f630b7d84cf06c",

#   "resource_link_title"=>"test app", "roles"=>"Instructor", "selection_directive"=>"embed_content",

#   "text"=>"modify", "tool_consumer_info_product_family_code"=>"canvas", "tool_consumer_info_version"=>"cloud",

#   "tool_consumer_instance_contact_email"=>"notifications@instructure.com", "tool_consumer_instance_guid"=>"ySt5cF5tiEU8j5oIzxT2J98caTu54Vl6y9s6gYdS:canvas-lms",

#   "tool_consumer_instance_name"=>"KTH Royal Institute of Technology", "user_id"=>"97d0ab13fafc432d23aa6cd73fc51d769d651f21", "oauth_signature"=>"4/+DYlNxEIa8ZvPEp2LmJ5Fp5Js="}

    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']

    #

    # to insert a URL

    #new_url=params['ext_content_return_url']+"?return_type=url&url="+CGI::escape("http://localhost:14921/text_to_return.html")+"&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

The URL insert produces:

<a title="inserted text" href="http://localhost:14921/text_to_return.html" target="_blank">&lt;p&gt;Some text to insert &lt;a href=" http://imscatalog.org/"&gt;catalog of certified products&lt;/a&gt; junkk junk and more junk&lt;/p&gt;</a>

The iframe inserts:

<iframe title="inserted text" src="http://localhost:14921/text_to_return.html" width="300" height="150" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen"></iframe>

text_to_return.html contains:

<p>Some text to insert <a href=\" http://imscatalog.org/\">catalog of certified

    products</a> junkk junk and more junk</p>

(2) I have been unable to get the ContentItems returns to work, when I add

"lti_message_type"=>"ContentItemSelectionRequest" to the button

and use content_type "application/vnd.ims.lti.v1.contentitems+json"

to try to directly return from the post "/lti_launch" do:

...

    {

      "lti_message_type" => "ContentItemSelection",

      "lti_version" => "LTI-1p0",

      "content_items" => {

        "@context" => "http://purl.imsglobal.org/ctx/lti/v1/ContentItem",

        "@graph" => [{

                     "@type" => "ContentItem",

                     "url" => "http://localhost:14921/text_to_return.html",

                     "title" =>  "inserted text",

                     "text" =>  string_to_insert,

                     "mediaType"=>  "text/html",

                     "placementAdvice" => { "presentationDocumentTarget" => "embed" }

                     }]

      }

   }.to_json

I do not get anything added to the text in the Rich Context Editor.

My goal is to try to insert the equivalent of a Word field so that I can add Zotero bibliographic citations and bibliographies to Canvas Web pages, see the attached.

5 Replies
chofer
Community Coach
Community Coach

 @maguire ​...

This sounds like a question for the Canvas Developers​ group here in the Canvas Community.  So, I will go ahead and share your question with that group.  If you are not following that group in the Canvas Community, you can do so by clicking on the link I've provided and then click on "Follow" at the top right corner of the page.  Also, I will tag your message with some keywords.  All of this should get your question some additional exposure here in the Community.  I hope this is helpful to you, Gerald!

Thanks for forwarding this. I you need source code, attached is the complete ruby sinatra file (it is based on the YouTube Search LTI example application (https://github.com/whitmer/youtube_lti) see attached. Also attached is the python code that I have used to examine and modify the button. (Note that I used the Emacs png file - as my initial intention was to add Emacs as an external editor to the Rich Context Editor.)

# original 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'

# 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'

#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.new(  File.open(File.join(CERT_PATH, "cert.pem")).read),

        :SSLPrivateKey      => OpenSSL::PKey::RSA.new(          File.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]

class MyServer  < Sinatra::Base

  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

# 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

# {"oauth_consumer_key"=>"consumerkey",

#   "oauth_signature_method"=>"HMAC-SHA1",

#   "oauth_timestamp"=>"1469633132", "oauth_nonce"=>"WBo7X2jy4RbwHOrpoMjktPcEflvZVBcVdvkLw1iJZE", "oauth_version"=>"1.0",

#   "context_id"=>"08d76eee8fbed3381e05c688c1f630b7d84cf06c", "context_label"=>"Chip", "context_title"=>"Chip sandbox", "custom_canvas_enrollment_state"=>"active", "custom_fooo"=>"125690",

#   "ext_content_intended_use"=>"embed", "ext_content_return_types"=>"oembed,lti_launch_url,url,image_url,iframe",

#   "ext_content_return_url"=>"https://kth.instructure.com/courses/11/external_content/success/external_tool_dialog",

#   "ext_roles"=>"urn:lti:instrole:ims/lis/Instructor,urn:lti:role:ims/lis/Instructor,urn:lti:sysrole:ims/lis/User",

#   "launch_presentation_document_target"=>"iframe", "launch_presentation_height"=>"500", "launch_presentation_locale"=>"en-GB",

#   "launch_presentation_return_url"=>"https://kth.instructure.com/courses/11/external_content/success/external_tool_dialog",

#   "launch_presentation_width"=>"500", "lti_message_type"=>"basic-lti-launch-request", "lti_version"=>"LTI-1p0",

#   "oauth_callback"=>"about:blank", "resource_link_id"=>"08d76eee8fbed3381e05c688c1f630b7d84cf06c",

#   "resource_link_title"=>"test app", "roles"=>"Instructor", "selection_directive"=>"embed_content",

#   "text"=>"modify", "tool_consumer_info_product_family_code"=>"canvas", "tool_consumer_info_version"=>"cloud",

#   "tool_consumer_instance_contact_email"=>"notifications@instructure.com", "tool_consumer_instance_guid"=>"ySt5cF5tiEU8j5oIzxT2J98caTu54Vl6y9s6gYdS:canvas-lms",

#   "tool_consumer_instance_name"=>"KTH Royal Institute of Technology", "user_id"=>"97d0ab13fafc432d23aa6cd73fc51d769d651f21", "oauth_signature"=>"4/+DYlNxEIa8ZvPEp2LmJ5Fp5Js="}

    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']

    #

    # to insert a URL

    #new_url=params['ext_content_return_url']+"?return_type=url&url="+CGI::escape("http://localhost:14921/text_to_return.html")+"&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

# 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

---------------------------------------------

#!  /usr/bin/env python3

#

# ./modify-external-tool-entry.py  course_id tool_id icon_url

#

# adds an editor icon for given tool_id for the given course_id

# the icon should be a 16x16 icon in png format

#

# G. Q: Maguire Jr.

#

# 2016.07.17

# revised 2016.07.21

#

import csv, requests, time

from pprint import pprint

import optparse

import sys

from lxml import html

import json

#############################

###### EDIT THIS STUFF ######

#############################

# styled based upon https://martin-thoma.com/configuration-files-in-python/

with open('config.json') as json_data_file:

       configuration = json.load(json_data_file)

       canvas = configuration['canvas']

       access_token= canvas["access_token"]

       # access_token=configuration["canvas"]["access_token"]

       #baseUrl = 'https://kth.instructure.com/api/v1/courses/' # changed to KTH domain

       baseUrl = 'https://%s/api/v1/courses/' % canvas.get('host', 'kth.instructure.com')

       header = {'Authorization' : 'Bearer ' + access_token}

#modules_csv = 'modules.csv' # name of file storing module names

log_file = 'log.txt' # a log file. it will log things

def write_to_log(message):

       with open(log_file, 'a') as log:

              log.write(message + "\n")

              pprint(message)

def add_editor_icon_url(course_id, external_tool_id, icon_url):

       # editor_button[url] string The url of the external tool

       # editor_button[enabled] boolean Set this to enable this feature

       # editor_button[icon_url] string The url of the icon to show in the WYSIWYG editor

       # editor_button[selection_width] string The width of the dialog the tool is launched in

       # editor_button[selection_height] string The height of the dialog the tool is launched in

       url = baseUrl + '%s/external_tools/%s' % (course_id, external_tool_id)

       if Verbose_Flag:

              print(url)

       payload={}

       # get the existing tool information

       r = requests.get(url, headers = header, data=payload)

       if Verbose_Flag:

              print("r.status_code=%d" % (r.status_code))

       if r.status_code == requests.codes.ok:

              tool_response = r.json() 

              print("Existing tool information for tool_id %s" % (external_tool_id))

              pprint(tool_response)

       else:

              print("No details for tool_id %s for course_id: %s" % (external_tool_id, course_id))

              return False

       # set editor button url to be the same as that of the tool

       tool_url=tool_response["url"]

             

       # added the message type to make the button do a ContentItemSelectionRequest

       payload={'editor_button[url]': tool_url,

                'editor_button[enabled]': True,

                'editor_button[icon_url]': icon_url,

                'editor_button[selection_width]': 500,

                'editor_button[selection_height]': 500,

                'editor_button[message_type]': "basic-lti-launch-request"

                #'editor_button[message_type]': "ContentItemSelectionRequest"

       }

       # set the tool information

       r = requests.put(url, headers = header, data=payload)

       if r.status_code == requests.codes.ok:

              tool_response = r.json() 

              pprint(tool_response)

              return tool_response

       else:

              print("Unable to add icon_url to tool_id %s for course_id: %s" % (external_tool_id, course_id))

              return False

       payload={}

       # get the updated tool information

       r = requests.get(url, headers = header, data=payload)

       if r.status_code == requests.codes.ok:

              tool_response = r.json() 

              print("Existing tool information for tool_id %s" % (external_tool_id))

              pprint(tool_response)

       return tool_response

def details_of_external_tools_for_course(course_id, external_tool_id):

       # Use the Canvas API to GET the tool's detailed information

       # GET /api/v1/courses/:course_id/external_tools/:external_tool_id

       # GET /api/v1/accounts/:account_id/external_tools/:external_tool_id

       url = baseUrl + '%s/external_tools/%s' % (course_id, external_tool_id)

       if Verbose_Flag:

              print(url)

       payload={}

       r = requests.get(url, headers = header, data=payload)

       if r.status_code == requests.codes.ok:

              tool_response = r.json() 

              pprint(tool_response)

              return tool_response

       else:

              print("No details for tool_id %s for course_id: %s" % (external_tool_id, course_id))

              return False

def list_external_tools_for_course(course_id):

       list_of_all_tools=[]

       # Use the Canvas API to get the list of external tools for this course

       # GET /api/v1/courses/:course_id/external_tools

       # GET /api/v1/accounts/:account_id/external_tools

       # GET /api/v1/groups/:group_id/external_tools

       url = baseUrl + '%s/external_tools' % (course_id)

       if Verbose_Flag:

              print("url: " + url)

       r = requests.get(url, headers = header)

       if Verbose_Flag:

              write_to_log("result of getting list of external tools: " + r.text)

       if r.status_code == requests.codes.ok:

              tool_response=r.json()

       else:

              print("No external tools for course_id: %s" % (course_id))

              return False

       for t_response in tool_response: 

              list_of_all_tools.append(t_response)

       # the following is needed when the reponse has been paginated

       # i.e., when the response is split into pieces - each returning only some of the list of modules

       # see "Handling Pagination" - Discussion created by tyler.clair@usu.edu on Apr 27, 2015, https://community.canvaslms.com/thread/1500

       while r.links['current']['url'] != r.links['last']['url']: 

              r = requests.get(r.links['next']['url'], headers=header) 

              tool_response = r.json() 

              for t_response in tool_response: 

                     list_of_all_tools.append(t_response)

       for t in list_of_all_tools:

              print("about to prettyprint tool: %s" % (t['name']))

              pprint(t)

       return list_of_all_tools

def main():

       global Verbose_Flag

       parser = optparse.OptionParser()

       parser.add_option('-v', '--verbose',

                         dest="verbose",

                         default=False,

                         action="store_true",

                         help="Print lots of output to stdout"

       )

       options, remainder = parser.parse_args()

       Verbose_Flag=options.verbose

       if Verbose_Flag:

              print('ARGV      :', sys.argv[1:])

              print('VERBOSE   :', options.verbose)

              print('REMAINING :', remainder)

       # add time stamp to log file

       log_time = str(time.asctime(time.localtime(time.time())))

       if Verbose_Flag:

              write_to_log(log_time)  

       if (len(remainder) < 3):

              print("Inusffient arguments\n must provide course_id external_tool_id, icon_url\n")

       else:

              course_id=remainder[0]

              external_tool_id=remainder[1]

              icon_url=remainder[2]

              output=add_editor_icon_url(course_id, external_tool_id, icon_url)

              if (output):

                     if Verbose_Flag:

                            pprint(output)

       # add time stamp to log file

       log_time = str(time.asctime(time.localtime(time.time())))

       if Verbose_Flag:

              write_to_log(log_time)  

              write_to_log("\n--DONE--\n\n")

if __name__ == "__main__": main()

Stefanie
Community Team
Community Team

Hello,  @maguire ! It's been about a month since we last heard from you, and I'm sorry to see that the Canvas Developers are, so far, stumped--so I've tagged them again to "bump" this thread. Were you ultimately able to devise a solution on your own? For now, I will mark this question as "Assumed Answered"; that will not prevent you or others from responding, so if you have in fact figured this out, please take a moment to update us.

Also, have you posted your question at the Instructure GitHub?  Home · instructure/canvas-lms Wiki · GitHub

I currently solve the problem by returning using a 'ext_content_return_url' and returning a URL with some text. This is not a satisfactory solution.

I have not posted a query to GitHub, but did raise the query through our CSM and the senior manager for customer success for the region I am in. They are supposed to have raised it to engineering. However, I have not yet heard any response.

Unfortunately, I have had to set this issues aside due to the start of the fall term. However, I have a student who is looking at the LTI to zotero tool provider that this question occurred in.

Thank you so much for the update,  @maguire ​! I'm hopeful that as your student makes progress in his investigation you will have a moment to keep us informed.