canvas-api 0.5 → 0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +83 -1
- data/lib/canvas-api.rb +103 -13
- metadata +1 -1
data/README.md
CHANGED
@@ -46,6 +46,29 @@ canvas.get("/api/v1/users/self/profile")
|
|
46
46
|
# => {id: 90210, name: "Annie Wilson", ... }
|
47
47
|
```
|
48
48
|
|
49
|
+
For POST and PUT requests the second parameter is the form parameters to append, either as a hash or
|
50
|
+
an array of arrays:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
54
|
+
canvas.put("/api/v1/users/self", {'user[name]' => 'Dixon Wilson', 'user[short_name]' => 'Dixon'})
|
55
|
+
# => {id: 90210, name: "Dixon Wilson", ... }
|
56
|
+
canvas.put("/api/v1/users/self", {'user' => {'name' => 'Dixon Wilson', 'short_name' => 'Dixon'}}) # this is synonymous with the previous call
|
57
|
+
# => {id: 90210, name: "Dixon Wilson", ... }
|
58
|
+
canvas.put("/api/v1/users/self", [['user[name]', 'Dixon Wilson'],['user[short_name]', 'Dixon']]) # this is synonymous with the previous call
|
59
|
+
# => {id: 90210, name: "Dixon Wilson", ... }
|
60
|
+
```
|
61
|
+
|
62
|
+
On GET requests you can either append query parameters to the actual path or as a hashed second argument:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
66
|
+
canvas.get("/api/v1/users/self/enrollments?type[]=TeacherEnrollment&type[]=TaEnrollment")
|
67
|
+
# => [{id: 1234, course_id: 5678, ... }, {id: 2345, course_id: 6789, ...}]
|
68
|
+
canvas.get("/api/v1/users/self/enrollments", {'type' => ['TeacherEnrollment', 'TaEnrollment']}) # this is synonymous with the previous call
|
69
|
+
# => [{id: 1234, course_id: 5678, ... }, {id: 2345, course_id: 6789, ...}]
|
70
|
+
```
|
71
|
+
|
49
72
|
### Pagination
|
50
73
|
|
51
74
|
API endpoints that return lists are often paginated, meaning they will only return the first X results
|
@@ -75,7 +98,66 @@ There are also some helper methods that can make some of the other tricky parts
|
|
75
98
|
|
76
99
|
#### File Uploads
|
77
100
|
|
78
|
-
|
101
|
+
Uploading files ia typically a multi-step process. There are three different ways to upload
|
102
|
+
files.
|
103
|
+
|
104
|
+
Upload a file from the local file system:
|
105
|
+
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
109
|
+
canvas.upload_file_from_local("/api/v1/users/self/files", File.open("/path/to/file.jpg"), :content_type => "image/jpeg")
|
110
|
+
# => {id: 1, display_name: "file.jpg", ... }
|
111
|
+
```
|
112
|
+
|
113
|
+
Upload a file synchronously from a remote URL:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
117
|
+
canvas.upload_file_from_url("/api/v1/users/self/files", :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
|
118
|
+
# => {id: 1, display_name: "image.jpg", ... }
|
119
|
+
```
|
120
|
+
|
121
|
+
Upload a file asynchronouysly from a remote URL:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
125
|
+
status_url = canvas.upload_file_from_url("/api/v1/users/self/files", :asynch => true, :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
|
126
|
+
# => "/api/v1/file_status/url"
|
127
|
+
canvas.get(status_url)
|
128
|
+
# => {upload_status: "pending"}
|
129
|
+
canvas.get(status_url)
|
130
|
+
# => {upload_status: "ready", attachment: {id: 1, display_name: "image.jpg", ... } }
|
131
|
+
```
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
135
|
+
status_url = canvas.upload_file_from_url("/api/v1/users/self/files", :asynch => true, :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
|
136
|
+
# => "/api/v1/file_status/url"
|
137
|
+
canvas.get(status_url)
|
138
|
+
# => {upload_status: "errored", message: "Invalid response code, expected 200 got 404"}
|
139
|
+
```
|
140
|
+
|
141
|
+
For any of these upload types you can optionally provide additional configuration parameters if
|
142
|
+
the upload endpoint is to an area of Canvas that supports folders (user files, course files, etc.)
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
canvas = Canvas::API.new(:host => "https://canvas.example.com", :token => "qwert")
|
146
|
+
#
|
147
|
+
# upload the file to a known folder with id 1234
|
148
|
+
canvas.upload_file_from_url("/api/v1/users/self/files", :parent_folder_id => 1234, :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
|
149
|
+
# => {id: 1, display_name: "image.jpg", ... }
|
150
|
+
#
|
151
|
+
# upload the file to a folder with the path "/friends"
|
152
|
+
canvas.upload_file_from_url("/api/v1/users/self/files", :parent_folder_path => "/friends", :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
|
153
|
+
# => {id: 1, display_name: "image.jpg", ... }
|
154
|
+
#
|
155
|
+
# rename this file instead of overwriting a file with the same name (overwrite is the default)
|
156
|
+
canvas.upload_file_from_url("/api/v1/users/self/files", :on_duplicate => "rename", :name => "image.jpg", :size => 12345, :url => "http://www.example.com/image.jpg")
|
157
|
+
# => {id: 1, display_name: "image.jpg", ... }
|
158
|
+
```
|
159
|
+
|
160
|
+
|
79
161
|
|
80
162
|
#### SIS ID Encoding
|
81
163
|
|
data/lib/canvas-api.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'cgi'
|
3
3
|
require 'net/http'
|
4
|
+
require 'net/http/post/multipart'
|
4
5
|
require 'json'
|
5
6
|
|
7
|
+
|
6
8
|
module Canvas
|
7
9
|
class API
|
8
10
|
def initialize(args={})
|
@@ -79,12 +81,22 @@ module Canvas
|
|
79
81
|
raise "invalid endpoint" unless (URI.parse(endpoint) rescue nil)
|
80
82
|
end
|
81
83
|
|
82
|
-
def generate_uri(endpoint)
|
84
|
+
def generate_uri(endpoint, params=nil)
|
83
85
|
validate_call(endpoint)
|
84
86
|
unless @token == "ignore"
|
85
87
|
endpoint += (endpoint.match(/\?/) ? "&" : "?") + "access_token=" + @token
|
86
88
|
endpoint += "&as_user_id=" + @as_user_id.to_s if @as_user_id
|
87
89
|
end
|
90
|
+
(params || {}).each do |key, value|
|
91
|
+
if value.is_a?(Array)
|
92
|
+
key = key + "[]" unless key.match(/\[\]/)
|
93
|
+
value.each do |val|
|
94
|
+
endpoint += (endpoint.match(/\?/) ? "&" : "?") + "#{CGI.escape(key.to_s)}=#{CGI.escape(val.to_s)}"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
endpoint += (endpoint.match(/\?/) ? "&" : "?") + "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
98
|
+
end
|
99
|
+
end
|
88
100
|
@uri = URI.parse(@host + endpoint)
|
89
101
|
@http = Net::HTTP.new(@uri.host, @uri.port)
|
90
102
|
@http.use_ssl = @uri.scheme == 'https'
|
@@ -121,8 +133,8 @@ module Canvas
|
|
121
133
|
Net::HTTP::Get.new(@uri.request_uri)
|
122
134
|
end
|
123
135
|
|
124
|
-
def get(endpoint)
|
125
|
-
generate_uri(endpoint)
|
136
|
+
def get(endpoint, params=nil)
|
137
|
+
generate_uri(endpoint, params)
|
126
138
|
request = get_request(endpoint)
|
127
139
|
retrieve_response(request)
|
128
140
|
end
|
@@ -134,30 +146,108 @@ module Canvas
|
|
134
146
|
end
|
135
147
|
|
136
148
|
def put(endpoint, params={})
|
137
|
-
generate_uri(endpoint)
|
149
|
+
generate_uri(endpoint, params['query_parameters'] || params[:query_parameters])
|
138
150
|
request = Net::HTTP::Put.new(@uri.request_uri)
|
139
|
-
request.set_form_data(params)
|
151
|
+
request.set_form_data(clean_params(params))
|
140
152
|
retrieve_response(request)
|
141
153
|
end
|
142
154
|
|
143
155
|
def post(endpoint, params={})
|
144
|
-
generate_uri(endpoint)
|
156
|
+
generate_uri(endpoint, params['query_parameters'] || params[:query_parameters])
|
145
157
|
request = Net::HTTP::Post.new(@uri.request_uri)
|
146
|
-
request.set_form_data(params)
|
158
|
+
request.set_form_data(clean_params(params))
|
147
159
|
retrieve_response(request)
|
148
160
|
end
|
161
|
+
|
162
|
+
def clean_params(params, prefix=nil)
|
163
|
+
params ||= {}
|
164
|
+
return params if params.is_a?(Array)
|
165
|
+
return nil unless params.is_a?(Hash)
|
166
|
+
params.delete(:query_parameters)
|
167
|
+
res = []
|
168
|
+
params.each do |key, val|
|
169
|
+
if val.is_a?(Array)
|
170
|
+
raise "No support for nested array parameters currently"
|
171
|
+
elsif val.is_a?(Hash)
|
172
|
+
res += clean_params(val, prefix ? (prefix + "[" + key.to_s + "]") : key.to_s)
|
173
|
+
else
|
174
|
+
if prefix
|
175
|
+
res << [prefix + "[" + key.to_s + "]", val.to_s]
|
176
|
+
else
|
177
|
+
res << [key.to_s, val.to_s]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
res
|
182
|
+
end
|
149
183
|
|
150
|
-
def upload_file_from_local
|
151
|
-
|
184
|
+
def upload_file_from_local(endpoint, file, opts={})
|
185
|
+
raise "Missing File object" unless file.is_a?(File)
|
186
|
+
params = {
|
187
|
+
:size => file.size,
|
188
|
+
:name => opts[:name] || opts['name'] || File.basename(file.path),
|
189
|
+
:content_type => opts[:content_type] || opts['content_type'] || "application/octet-stream",
|
190
|
+
:parent_folder_id => opts[:parent_folder_id] || opts['parent_folder_id'],
|
191
|
+
:parent_folder_path => opts[:parent_folder_path] || opts['parent_folder_path'],
|
192
|
+
:on_duplicate => opts[:on_duplicate] || opts['on_duplicate']
|
193
|
+
}
|
194
|
+
res = post(endpoint, params)
|
195
|
+
if !res['upload_url']
|
196
|
+
raise ApiError.new("Unexpected error: #{res['message'] || 'no upload URL returned'}")
|
197
|
+
end
|
198
|
+
status_url = multipart_upload(res['upload_url'], res['upload_params'], params, file)
|
199
|
+
status_path = "/" + status_url.split(/\//, 4)[-1]
|
200
|
+
res = get(status_path)
|
201
|
+
res
|
202
|
+
end
|
203
|
+
|
204
|
+
def multipart_upload(url, upload_params, params, file)
|
205
|
+
uri = URI.parse(url)
|
206
|
+
@multipart_args = upload_params.merge({})
|
207
|
+
@multipart_args['file'] = UploadIO.new(file, params[:content_type], params[:name])
|
208
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
209
|
+
http.use_ssl = uri.scheme == 'https'
|
210
|
+
req = Net::HTTP::Post::Multipart.new(uri.path, @multipart_args)
|
211
|
+
res = http.start do |stream_http|
|
212
|
+
stream_http.request(req)
|
213
|
+
end
|
214
|
+
raise ApiError.new("Unexpected error: #{res.body}") if !res['Location']
|
215
|
+
res['Location']
|
152
216
|
end
|
153
217
|
|
154
|
-
def upload_file_from_url
|
155
|
-
|
218
|
+
def upload_file_from_url(endpoint, opts)
|
219
|
+
asynch = opts.delete('asynch') || opts.delete(:asynch)
|
220
|
+
['url', 'name', 'size'].each do |k|
|
221
|
+
raise "Missing value: #{k}" unless opts[k.to_s] || opts[k.to_sym]
|
222
|
+
end
|
223
|
+
|
224
|
+
res = post(endpoint, opts)
|
225
|
+
status_url = res['status_url']
|
226
|
+
if !status_url
|
227
|
+
raise ApiError.new("Unexpected error: #{res['message'] || 'no status URL returned'}")
|
228
|
+
end
|
229
|
+
status_path = "/" + status_url.split(/\//, 4)[-1]
|
230
|
+
if asynch
|
231
|
+
return status_path
|
232
|
+
else
|
233
|
+
attachment = nil
|
234
|
+
while !attachment
|
235
|
+
res = get(status_path)
|
236
|
+
if res['upload_status'] == 'errored'
|
237
|
+
raise ApiError.new(res['message'])
|
238
|
+
elsif !res['upload_status']
|
239
|
+
raise ApiError.new("Unexpected response")
|
240
|
+
end
|
241
|
+
status_path = res['status_path'] if res['status_path']
|
242
|
+
attachment = res['attachment']
|
243
|
+
sleep (defined?(SLEEP_TIME) ? SLEEP_TIME : 5) unless attachment
|
244
|
+
end
|
245
|
+
attachment
|
246
|
+
end
|
156
247
|
end
|
157
248
|
end
|
158
249
|
|
159
|
-
class ApiError < StandardError
|
160
|
-
end
|
250
|
+
class ApiError < StandardError; end
|
161
251
|
|
162
252
|
class ResultSet < Array
|
163
253
|
def initialize(api, arr)
|