forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcanvadocs.rb
376 lines (330 loc) · 12.8 KB
/
canvadocs.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# frozen_string_literal: true
#
# Copyright (C) 2014 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require 'cgi'
require 'net/http'
require 'net/https'
require 'json'
require_dependency 'canvadocs/session'
module Canvadocs
extend CanvadocsHelper
RENDER_O365 = 'office_365'
RENDER_BOX = 'box_view'
RENDER_CROCODOC = 'crocodoc'
RENDER_PDFJS = 'pdfjs'
# Public: A small ruby client that wraps the Box View api.
#
# Examples
#
# Canvadocs::API.new(:token => <token>)
class API
attr_accessor :token, :http, :url
# Public: The base part of the url that is the same for all api requests.
BASE_URL = "https://view-api.box.com/1"
# Public: Initialize a Canvadocs api object
#
# opts - A hash of options with which to initialize the object
# :token - The api token to use to authenticate requests. Required.
#
# Examples
# crocodoc = Canvadocs::API.new(:token => <token>)
# # => <Canvadocs::API:<id>>
def initialize(opts)
self.token = opts[:token]
_, @url = CanvasHttp.validate_url(opts[:base_url] || BASE_URL)
@http = CanvasHttp.connection_for_uri(@url)
end
# -- Documents --
# Public: Create a document with the file at the given url.
#
# obj - a url string
# params - other post params
#
# Examples
#
# upload("http://www.example.com/test.doc")
# # => { "id": 1234, "status": "queued" }
#
# Returns a hash containing the document's id and status
def upload(obj, extra_params = {})
params = if obj.is_a?(File)
{ file: obj }.merge(extra_params)
raise Canvadocs::Error, "TODO: support raw files"
else
{ url: obj.to_s }.merge(extra_params)
end
raw_body = api_call(:post, "documents", params)
JSON.parse(raw_body)
end
# Public: Delete a document.
#
# id - a single document id to delete
#
def delete(id)
api_call(:delete, "documents/#{id}")
end
# -- Sessions --
# Public: Create a session, which is a unique id with which you can view
# the document. Sessions expire 60 minutes after they are generated.
#
# id - The id of the document for the session
#
# Examples
#
# session(1234)
# # => { "id": "CFAmd3Qjm_2ehBI7HyndnXKsDrQXJ7jHCuzcRv" }
#
# Returns a hash containing the session id
def session(document_id, opts={})
raw_body = api_call(:post, "sessions",
opts.merge(:document_id => document_id))
JSON.parse(raw_body)
end
# Public: Get the url for the viewer for a session.
#
# session_id - The id of the session (see #session)
#
# Examples
# view("CFAmd3Qjm_2ehBI7HyndnXKsDrQXJ7jHCuzcRv_V4FAgbSmaBkF")
# # => https://view-api.box.com/1/sessions/#{session_id}/view?theme=dark"
#
# Returns a url string for viewing the session
def view(session_id)
"#{@url}/sessions/#{session_id}/view?theme=dark"
end
# -- API Glue --
# Internal: Setup the api call, format the parameters, send the request,
# parse the response and return it.
#
# method - The http verb to use, currently :get or :post
# endpoint - The api endpoint to hit. this is the part after
# +base_url+. please do not include a beginning slash.
# params - Parameters to send with the api call
#
# Examples
#
# api_call(:post,
# "documents",
# { url: "http://www.example.com/test.doc" })
# # => { "id": 1234 }
#
# Returns the json parsed response body of the call
def api_call(method, endpoint, params={})
# dispatch to the right method, with the full path (/api/v2 + endpoint)
request = self.send("format_#{method}", "#{@url.path}/#{endpoint}", params)
request["Authorization"] = "Token #{token}"
response = @http.request(request)
unless response.code =~ /\A20./
err_message = "HTTP Error #{response.code}: #{response.body}"
klass = Canvadocs::HttpError
klass = Canvadocs::ServerError if response.code.to_s == "500"
klass = Canvadocs::BadGateway if response.code.to_s == "502"
klass = Canvadocs::BadRequest if response.code.to_s == "400"
raise klass, err_message
end
response.body
end
# Internal: Format and create a Net::HTTP get request, with query
# parameters.
#
# path - the path to get
# params - the params to add as query params to the path
#
# Examples
#
# format_get("/api/v2/document/status",
# { :token => <token>, :uuids => <uuids> })
# # => <Net::HTTP::Get:<id>> for
# # "/api/v2/document/status?token=<token>&uuids=<uuids>"
#
# Returns a Net::HTTP::Get object for the path with query params
def format_get(path, params)
query = params.map { |k,v| "#{k}=#{CGI::escape(v.to_s)}" }.join("&")
Net::HTTP::Get.new("#{path}?#{query}")
end
# Internal: Format and create a Net::HTTP post request, with form
# parameters.
#
# path - the path to get
# params - the params to add as form params to the path
#
# Examples
#
# format_post("/api/v2/document/upload",
# { :token => <token>, :url => <url> })
# # => <Net::HTTP::Post:<id>>
#
# Returns a Net::HTTP::Post object for the path with json-formatted params
def format_post(path, params)
Net::HTTP::Post.new(path).tap { |req|
req["Content-Type"] = "application/json"
req.body = params.to_json
}
end
end
class Error < StandardError; end
class HttpError < Error; end
class ServerError < HttpError; end
class BadGateway < HttpError; end
class BadRequest < HttpError; end
def self.config
PluginSetting.settings_for_plugin(:canvadocs)
end
def self.enabled?
!!config
end
def self.annotations_supported?
enabled? && Canvas::Plugin.value_to_boolean(config["annotations_supported"])
end
# annotations_supported? calls enabled?
def self.hijack_crocodoc_sessions?
annotations_supported? && Canvas::Plugin.value_to_boolean(config["hijack_crocodoc_sessions"])
end
def self.user_session_params(current_user, attachment: nil, submission: nil)
if submission.nil?
return {} if attachment.nil?
submission = Submission.find_by(
id: AttachmentAssociation.where(context_type: 'Submission', attachment: attachment).select(:context_id)
)
return {} if submission.nil?
end
assignment = submission.assignment
enrollments = assignment.course.enrollments.index_by(&:user_id)
session_params = current_user_session_params(submission, current_user, enrollments)
# Graders may not have access to other graders' annotations if the
# assignment is set that way.
if assignment.moderated_grading?
session_params[:user_filter] = moderated_grading_user_filter(submission, current_user, enrollments)
session_params[:restrict_annotations_to_user_filter] = true
elsif assignment.anonymize_students?
session_params[:user_filter] = anonymous_unmoderated_user_filter(submission, current_user, enrollments)
session_params[:restrict_annotations_to_user_filter] = current_user.id == submission.user_id
end
# Set visibility for students and peer reviewers.
if !submission.user_can_read_grade?(current_user)
session_params[:restrict_annotations_to_user_filter] = true
session_params[:user_filter] ||= [
user_filter_entry(
current_user,
submission,
role: canvadocs_user_role(submission.assignment.course, current_user, enrollments),
anonymize: false
)
]
if assignment.peer_reviews?
session_params[:user_filter] = session_params[:user_filter] | peer_review_user_filter(submission, current_user, enrollments)
end
end
# For Student Annotation assignments, the Canvadoc won't have a
# relationship with the Submission through CanvadocsSubmission, so we set
# a blank user_filter here to avoid the default of restricting to only the
# viewing user's annotations (default is in Canvadocs::Session). If we the
# user_filter is nil here, then we know we didn't have any other reason to
# be restrictive.
if session_params[:user_filter].nil? && submission.submission_type == "student_annotation"
session_params[:restrict_annotations_to_user_filter] = false
session_params[:user_filter] = []
end
session_params
end
class << self
private
def current_user_session_params(submission, current_user, enrollments)
# Always include the current user's anonymous ID and real ID, regardless
# of the settings for the current assignment
current_user_params = {
user: current_user,
user_id: canvadocs_user_id(current_user),
user_role: canvadocs_user_role(submission.assignment.course, current_user, enrollments),
user_name: canvadocs_user_name(current_user)
}
# Not calling submission.anonymous_identities here because we want to include
# anonymous_ids for moderation_graders that have not yet taken a slot
anonymous_id = if submission.user_id == current_user.id
submission.anonymous_id
elsif submission.assignment.moderated_grading?
submission.assignment.moderation_graders.find_by(user: current_user)&.anonymous_id
end
current_user_params[:user_anonymous_id] = anonymous_id if anonymous_id
current_user_params
end
def peer_review_user_filter(submission, current_user, enrollments)
# Submitter, instructors, and admins should always see assessors' annotations.
is_instructor = submission.course.participating_instructors.include?(current_user)
is_admin = submission.course.account_membership_allows(current_user)
users_for_filter = if current_user == submission.user || is_instructor || is_admin
User.where(id: submission.assessment_requests.pluck(:assessor_id)).to_a
else
[]
end
# The current user's annotations should always be visible.
users_for_filter.push(current_user)
# When the submission is for a Student Annotation assignment, the
# annotations are the submission, so add the submitter.
if submission.submission_type == "student_annotation"
users_for_filter.push(submission.user)
end
users_for_filter.map do |user|
user_filter_entry(
user,
submission,
role: canvadocs_user_role(submission.assignment.course, user, enrollments),
anonymize: submission.assignment.anonymous_peer_reviews?
)
end
end
def moderated_grading_user_filter(submission, current_user, enrollments)
submission.moderation_allow_list_for_user(current_user).map do |user|
user_filter_entry(
user,
submission,
role: canvadocs_user_role(submission.assignment.course, user, enrollments),
anonymize: anonymize_user_for_moderated_assignment?(user, current_user, submission),
)
end
end
def anonymous_unmoderated_user_filter(submission, current_user, enrollments)
[user_filter_entry(
submission.user,
submission,
role: canvadocs_user_role(submission.assignment.course, submission.user, enrollments),
anonymize: current_user.id != submission.user_id
)]
end
def anonymize_user_for_moderated_assignment?(user, current_user, submission)
# You never see your own annotations anonymized, and students never see anonymized annotations from anyone.
return false if current_user == user || current_user == submission.user
if user == submission.user
submission.assignment.anonymize_students?
else
!submission.assignment.can_view_other_grader_identities?(current_user)
end
end
def user_filter_entry(user, submission, role:, anonymize:)
if anonymize
id = submission.anonymous_identities.dig(user.id, :id).to_s
type = 'anonymous'
name = submission.anonymous_identities.dig(user.id, :name)
else
id = user.global_id.to_s
type = 'real'
name = canvadocs_user_name(user)
end
{ id: id, type: type, role: role, name: name }
end
end
end