canvas_interactor 1.0.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +86 -0
- data/Rakefile +19 -0
- data/app/assets/config/canvas_interactor_manifest.js +2 -0
- data/app/assets/javascripts/canvas_interactor/application.js +15 -0
- data/app/assets/stylesheets/canvas_interactor/application.css +15 -0
- data/app/controllers/canvas_interactor/application_controller.rb +6 -0
- data/app/controllers/canvas_interactor/canvas_controller.rb +25 -0
- data/app/helpers/canvas_interactor/application_helper.rb +4 -0
- data/app/jobs/canvas_interactor/application_job.rb +4 -0
- data/app/mailers/canvas_interactor/application_mailer.rb +6 -0
- data/app/models/canvas_interactor/application_record.rb +5 -0
- data/app/models/canvas_interactor/authorization.rb +26 -0
- data/app/views/layouts/canvas_interactor/application.html.erb +16 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20180607153145_create_canvas_interactor_authorizations.rb +12 -0
- data/lib/canvas_interactor.rb +21 -0
- data/lib/canvas_interactor/canvas_api.rb +249 -0
- data/lib/canvas_interactor/canvas_api_extensions.rb +8 -0
- data/lib/canvas_interactor/canvas_application.rb +65 -0
- data/lib/canvas_interactor/canvas_config.rb +28 -0
- data/lib/canvas_interactor/config.rb +3 -0
- data/lib/canvas_interactor/engine.rb +5 -0
- data/lib/canvas_interactor/version.rb +3 -0
- data/lib/tasks/canvas_interactor_tasks.rake +4 -0
- metadata +259 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9b740eae5b741f2cc3cb016872d0c1de2c187da0340f00427de63b99d3821077
|
4
|
+
data.tar.gz: 8bdb6648bb107a0a6abe6ffce96b61f63e1aa28bc3218347921a24f15294776b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c903d982ff10c452e26953dbf43ce9d9bae5dc93719c3461f4f36a54481c2cd198b4f3baa19256c88e9f7041ab87f03169d3aba768236939aba6f00c75c20ed2
|
7
|
+
data.tar.gz: 5fef267c4effc9b4d66493893bfc2767fe5363c964f8663c65f373272ca318f6fe48036063dfebf8e8ce70810d140191514501eb4e1f47331128cf40b1228e8c
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Ahmad Hassan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# CanvasInteractor
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'canvas_interactor'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install canvas_interactor
|
23
|
+
```
|
24
|
+
|
25
|
+
1.To copy these migrations into the application and migrate run the following command
|
26
|
+
|
27
|
+
```
|
28
|
+
$ bundle install
|
29
|
+
$ bundle exec rake railties:install:migrations
|
30
|
+
$ bundle exec rake db:migrate
|
31
|
+
```
|
32
|
+
|
33
|
+
* If you would like to run migrations only from one engine, you can do it by specifying SCOPE:
|
34
|
+
|
35
|
+
`$ bin/rails db:migrate SCOPE=canvas_interactor`
|
36
|
+
|
37
|
+
2.Then, mount the engine to your app by adding this line to your routes.rb file
|
38
|
+
|
39
|
+
`mount CanvasInteractor::Engine => "/canvas_interactor"`
|
40
|
+
|
41
|
+
3.Next, include the engine in your ApplicationController
|
42
|
+
|
43
|
+
```
|
44
|
+
class ApplicationController < ActionController::Base
|
45
|
+
include CanvasOauth::CanvasApplication
|
46
|
+
|
47
|
+
...
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
After that, create an `canvas.yml` file in your `config/` folder that looks something like this (or see `config/canvas.yml.example` for a template):
|
52
|
+
|
53
|
+
```
|
54
|
+
default: &default
|
55
|
+
key: your_key
|
56
|
+
secret: your_secret
|
57
|
+
|
58
|
+
development:
|
59
|
+
<<: *default
|
60
|
+
|
61
|
+
test:
|
62
|
+
<<: *default
|
63
|
+
|
64
|
+
production:
|
65
|
+
<<: *default
|
66
|
+
```
|
67
|
+
## Usage
|
68
|
+
|
69
|
+
TODO:
|
70
|
+
|
71
|
+
## Configuring the Tool Consumer
|
72
|
+
|
73
|
+
You will need a developer key and secret from canvas, which should be entered into you `canvas.yml` file.
|
74
|
+
|
75
|
+
## Quick Start
|
76
|
+
|
77
|
+
TODO:
|
78
|
+
|
79
|
+
## Contributing
|
80
|
+
Contributors are welcome.
|
81
|
+
|
82
|
+
`er.ahmad.hassan@gmail.com`
|
83
|
+
https://twitter.com/TheAhmadHassanK
|
84
|
+
|
85
|
+
## License
|
86
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'CanvasInteractor'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
load 'rails/tasks/statistics.rake'
|
18
|
+
|
19
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module CanvasInteractor
|
2
|
+
class CanvasController < CanvasInteractor::ApplicationController
|
3
|
+
skip_before_action :request_canvas_authentication
|
4
|
+
|
5
|
+
def oauth
|
6
|
+
if verify_oauth2_state(params[:state]) && params[:code]
|
7
|
+
if token = canvas.get_access_token(params[:code])
|
8
|
+
if CanvasInteractor::Authorization.cache_token(token, user_id, tool_consumer_instance_guid)
|
9
|
+
redirect_to main_app.root_path
|
10
|
+
else
|
11
|
+
render text: "Error: unable to save token"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
render text: "Error: invalid code - #{params[:code]}"
|
15
|
+
end
|
16
|
+
else
|
17
|
+
render text: "#{CanvasInteractor::Config.tool_name} needs access to your account in order to function properly. Please try again and click log in to approve the integration."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def verify_oauth2_state(callback_state)
|
22
|
+
callback_state.present? && callback_state == session.delete(:oauth2_state)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CanvasInteractor
|
2
|
+
class Authorization < ApplicationRecord
|
3
|
+
validates :canvas_user_id, :token, :last_used_at, presence: true
|
4
|
+
|
5
|
+
def self.cache_token(token, user_id, tool_consumer_instance_guid)
|
6
|
+
create do |t|
|
7
|
+
t.token = token
|
8
|
+
t.canvas_user_id = user_id
|
9
|
+
t.tool_consumer_instance_guid = tool_consumer_instance_guid
|
10
|
+
t.last_used_at = Time.now
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.fetch_token(user_id, tool_consumer_instance_guid)
|
15
|
+
user_tokens = where(canvas_user_id: user_id, tool_consumer_instance_guid: tool_consumer_instance_guid).order('created_at DESC')
|
16
|
+
if canvas_auth = user_tokens.first
|
17
|
+
canvas_auth.update_attribute(:last_used_at, Time.now)
|
18
|
+
return canvas_auth.token
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.clear_tokens(user_id, tool_consumer_instance_guid)
|
23
|
+
where(canvas_user_id: user_id, tool_consumer_instance_guid: tool_consumer_instance_guid).destroy_all
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Canvas interactor</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "canvas_interactor/application", media: "all" %>
|
9
|
+
<%= javascript_include_tag "canvas_interactor/application" %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
|
13
|
+
<%= yield %>
|
14
|
+
|
15
|
+
</body>
|
16
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateCanvasInteractorAuthorizations < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :canvas_interactor_authorizations do |t|
|
4
|
+
t.integer :canvas_user_id
|
5
|
+
t.string :tool_consumer_instance_guid, :null => false
|
6
|
+
t.string :token
|
7
|
+
t.datetime :last_used_at
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'canvas_interactor/engine'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'httparty'
|
4
|
+
require 'link_header'
|
5
|
+
require 'canvas_interactor/config'
|
6
|
+
require 'canvas_interactor/canvas_application'
|
7
|
+
require 'canvas_interactor/canvas_api'
|
8
|
+
require 'canvas_interactor/canvas_api_extensions'
|
9
|
+
require 'canvas_interactor/canvas_config'
|
10
|
+
|
11
|
+
module CanvasInteractor
|
12
|
+
mattr_accessor :app_root
|
13
|
+
|
14
|
+
def self.setup
|
15
|
+
yield self
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.config
|
19
|
+
yield(CanvasInteractor::Config)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
module CanvasInteractor
|
2
|
+
class CanvasApi
|
3
|
+
include HTTParty
|
4
|
+
PER_PAGE = 50
|
5
|
+
|
6
|
+
attr_accessor :token, :key, :secret
|
7
|
+
attr_reader :canvas_url
|
8
|
+
|
9
|
+
def initialize(canvas_url, token, key, secret)
|
10
|
+
self.canvas_url = canvas_url
|
11
|
+
self.token = token
|
12
|
+
self.key = key
|
13
|
+
self.secret = secret
|
14
|
+
end
|
15
|
+
|
16
|
+
def authenticated_request(method, *params)
|
17
|
+
params << {} if params.size == 1
|
18
|
+
|
19
|
+
params.last[:headers] ||= {}
|
20
|
+
params.last[:headers]['Authorization'] = "Bearer #{token}"
|
21
|
+
|
22
|
+
start = Time.now
|
23
|
+
|
24
|
+
response = self.class.send(method, *params)
|
25
|
+
|
26
|
+
Rails.logger.info {
|
27
|
+
stop = Time.now
|
28
|
+
elapsed = ((stop - start) * 1000).round(2)
|
29
|
+
|
30
|
+
params.last[:headers].reject! { |k| k == 'Authorization' }
|
31
|
+
"API call (#{elapsed}ms): #{method} #{params.inspect}"
|
32
|
+
}
|
33
|
+
|
34
|
+
if response && response.unauthorized?
|
35
|
+
if response.headers['WWW-Authenticate'].present?
|
36
|
+
raise CanvasApi::Authenticate
|
37
|
+
else
|
38
|
+
raise CanvasApi::Unauthorized
|
39
|
+
end
|
40
|
+
else
|
41
|
+
return response
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def paginated_get(*params)
|
46
|
+
params[1] ||= {}
|
47
|
+
params[1][:query] ||= {}
|
48
|
+
params[1][:query][:per_page] = PER_PAGE
|
49
|
+
|
50
|
+
all_pages = []
|
51
|
+
|
52
|
+
while params[0] do
|
53
|
+
if current_page = authenticated_get(*params)
|
54
|
+
all_pages += current_page if valid_page?(current_page)
|
55
|
+
|
56
|
+
links = LinkHeader.parse(current_page.headers['link'])
|
57
|
+
params[0] = links.find_link(["rel", "next"]).try(:href)
|
58
|
+
else
|
59
|
+
params[0] = nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
all_pages
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_report(account_id, report_type, params)
|
67
|
+
report = authenticated_post("/api/v1/accounts/#{account_id}/reports/#{report_type}", { body: params })
|
68
|
+
report = authenticated_get "/api/v1/accounts/#{account_id}/reports/#{report_type}/#{report['id']}"
|
69
|
+
while report['status'] == 'running'
|
70
|
+
sleep(4)
|
71
|
+
report = authenticated_get "/api/v1/accounts/#{account_id}/reports/#{report_type}/#{report['id']}"
|
72
|
+
end
|
73
|
+
|
74
|
+
if report['status'] == 'complete'
|
75
|
+
file_id = report['file_url'].match(/files\/([0-9]+)\/download/)[1]
|
76
|
+
file = get_file(file_id)
|
77
|
+
return hash_csv(self.class.get(file['url'], limit: 15).parsed_response)
|
78
|
+
else
|
79
|
+
return report
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def valid_page?(page)
|
84
|
+
page && page.size > 0
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_file(file_id)
|
88
|
+
authenticated_get "/api/v1/files/#{file_id}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_accounts_provisioning_report(account_id)
|
92
|
+
get_report(account_id, :provisioning_csv, 'parameters[accounts]' => true)
|
93
|
+
end
|
94
|
+
|
95
|
+
#Needs to be refactored to somewhere more generic
|
96
|
+
def hash_csv(csv_string)
|
97
|
+
require 'csv'
|
98
|
+
|
99
|
+
csv = CSV.parse(csv_string)
|
100
|
+
headers = csv.shift
|
101
|
+
output = []
|
102
|
+
|
103
|
+
csv.each do |row|
|
104
|
+
hash = {}
|
105
|
+
headers.each do |header|
|
106
|
+
hash[header] = row.shift.to_s
|
107
|
+
end
|
108
|
+
output << hash
|
109
|
+
end
|
110
|
+
|
111
|
+
return output
|
112
|
+
end
|
113
|
+
|
114
|
+
def authenticated_get(*params)
|
115
|
+
authenticated_request(:get, *params)
|
116
|
+
end
|
117
|
+
|
118
|
+
def authenticated_post(*params)
|
119
|
+
authenticated_request(:post, *params)
|
120
|
+
end
|
121
|
+
|
122
|
+
def authenticated_put(*params)
|
123
|
+
authenticated_request(:put, *params)
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_courses
|
127
|
+
paginated_get "/api/v1/courses"
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_account(account_id)
|
131
|
+
authenticated_get "/api/v1/accounts/#{account_id}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_account_sub_accounts(account_id)
|
135
|
+
paginated_get "/api/v1/accounts/#{account_id}/sub_accounts", { query: { :recursive => true } }
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_account_courses(account_id)
|
139
|
+
paginated_get "/api/v1/accounts/#{account_id}/courses"
|
140
|
+
end
|
141
|
+
|
142
|
+
def get_account_users(account_id)
|
143
|
+
paginated_get "/api/v1/accounts/#{account_id}/users"
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_course(course_id)
|
147
|
+
authenticated_get "/api/v1/courses/#{course_id}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def get_section_enrollments(section_id)
|
151
|
+
paginated_get "/api/v1/sections/#{section_id}/enrollments"
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_user_enrollments(user_id)
|
155
|
+
paginated_get "/api/v1/users/#{user_id}/enrollments"
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_course_users(course_id)
|
159
|
+
paginated_get "/api/v1/courses/#{course_id}/users"
|
160
|
+
end
|
161
|
+
|
162
|
+
def get_all_course_users(course_id)
|
163
|
+
paginated_get "/api/v1/courses/#{course_id}/users", { query: {enrollment_state: ["active","invited","rejected","completed","inactive"] } }
|
164
|
+
end
|
165
|
+
|
166
|
+
def get_course_teachers_and_tas(course_id)
|
167
|
+
paginated_get "/api/v1/courses/#{course_id}/users", { query: { enrollment_type: ['teacher', 'ta'] } }
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_course_students(course_id)
|
171
|
+
paginated_get "/api/v1/courses/#{course_id}/students"
|
172
|
+
end
|
173
|
+
|
174
|
+
def get_section(section_id)
|
175
|
+
authenticated_get "/api/v1/sections/#{section_id}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_sections(course_id)
|
179
|
+
paginated_get "/api/v1/courses/#{course_id}/sections", { query: { :include => ['students', 'avatar_url', 'enrollments'] } }
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_assignments(course_id)
|
183
|
+
paginated_get "/api/v1/courses/#{course_id}/assignments"
|
184
|
+
end
|
185
|
+
|
186
|
+
def get_assignment(course_id, assignment_id)
|
187
|
+
authenticated_get "/api/v1/courses/#{course_id}/assignments/#{assignment_id}"
|
188
|
+
end
|
189
|
+
|
190
|
+
def get_user_profile(user_id)
|
191
|
+
authenticated_get "/api/v1/users/#{user_id}/profile"
|
192
|
+
end
|
193
|
+
|
194
|
+
def create_assignment(course_id, params)
|
195
|
+
authenticated_post "/api/v1/courses/#{course_id}/assignments", { body: { assignment: params } }
|
196
|
+
end
|
197
|
+
|
198
|
+
def grade_assignment(course_id, assignment_id, user_id, params)
|
199
|
+
authenticated_put "/api/v1/courses/#{course_id}/assignments/#{assignment_id}/submissions/#{user_id}", { body: params }
|
200
|
+
end
|
201
|
+
|
202
|
+
def course_account_id(course_id)
|
203
|
+
course = get_course(course_id)
|
204
|
+
course['account_id'] if course
|
205
|
+
end
|
206
|
+
|
207
|
+
def root_account_id(account_id)
|
208
|
+
if account_id && account = get_account(account_id)
|
209
|
+
root_id = account['root_account_id']
|
210
|
+
end
|
211
|
+
|
212
|
+
root_id || account_id
|
213
|
+
end
|
214
|
+
|
215
|
+
def course_root_account_id(course_id)
|
216
|
+
root_account_id(course_account_id(course_id))
|
217
|
+
end
|
218
|
+
|
219
|
+
def auth_url(redirect_uri, oauth2_state)
|
220
|
+
"#{canvas_url}/login/oauth2/auth?client_id=#{key}&response_type=code&state=#{oauth2_state}&redirect_uri=#{redirect_uri}"
|
221
|
+
end
|
222
|
+
|
223
|
+
def get_access_token(code)
|
224
|
+
params = {
|
225
|
+
body: {
|
226
|
+
client_id: key,
|
227
|
+
client_secret: secret,
|
228
|
+
code: code
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
response = self.class.post '/login/oauth2/token', params
|
233
|
+
self.token = response['access_token']
|
234
|
+
end
|
235
|
+
|
236
|
+
def hex_sis_id(name, value)
|
237
|
+
hex = value.unpack("H*")[0]
|
238
|
+
return "hex:#{name}:#{hex}"
|
239
|
+
end
|
240
|
+
|
241
|
+
def canvas_url=(value)
|
242
|
+
@canvas_url = value
|
243
|
+
self.class.base_uri(value)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class CanvasApi::Unauthorized < StandardError ; end
|
248
|
+
class CanvasApi::Authenticate < StandardError ; end
|
249
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module CanvasInteractor
|
2
|
+
class CanvasApiExtensions
|
3
|
+
def self.build(canvas_url, user_id, tool_consumer_instance_guid)
|
4
|
+
token = CanvasInteractor::Authorization.fetch_token(user_id, tool_consumer_instance_guid)
|
5
|
+
CanvasApi.new(canvas_url, token, CanvasConfig.key, CanvasConfig.secret)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module CanvasInteractor
|
2
|
+
module CanvasApplication
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
included do
|
9
|
+
helper_method :canvas
|
10
|
+
|
11
|
+
rescue_from CanvasApi::Authenticate, with: :reauthenticate
|
12
|
+
rescue_from CanvasApi::Unauthorized, with: :unauthorized_canvas_access
|
13
|
+
before_action :request_canvas_authentication
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
def initialize_canvas
|
18
|
+
@canvas = ::CanvasInteractor::CanvasApiExtensions.build(canvas_url, user_id, tool_consumer_instance_guid)
|
19
|
+
end
|
20
|
+
|
21
|
+
def canvas
|
22
|
+
@canvas || initialize_canvas
|
23
|
+
end
|
24
|
+
|
25
|
+
def canvas_token
|
26
|
+
::CanvasInteractor::Authorization.fetch_token(user_id, tool_consumer_instance_guid)
|
27
|
+
end
|
28
|
+
|
29
|
+
def request_canvas_authentication
|
30
|
+
if !params[:code].present? && !canvas_token.present?
|
31
|
+
session[:oauth2_state] = SecureRandom.urlsafe_base64(24)
|
32
|
+
redirect_to canvas.auth_url(canvas_interactor_url, session[:oauth2_state])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def not_acceptable
|
37
|
+
render text: "Unable to process request", status: 406
|
38
|
+
end
|
39
|
+
|
40
|
+
def unauthorized_canvas_access
|
41
|
+
render text: "Your Canvas Developer Key is not authorized to access this data.", status: 401
|
42
|
+
end
|
43
|
+
|
44
|
+
def reauthenticate
|
45
|
+
::CanvasInteractor::Authorization.clear_tokens(user_id, tool_consumer_instance_guid)
|
46
|
+
request_canvas_authentication
|
47
|
+
end
|
48
|
+
|
49
|
+
# these next three methods rely on external session data and either need to
|
50
|
+
# be overridden or the session data needs to be set up by the time the
|
51
|
+
# oauth filter runs (like with the lti_provider_engine)
|
52
|
+
|
53
|
+
def canvas_url
|
54
|
+
session[:canvas_url]
|
55
|
+
end
|
56
|
+
|
57
|
+
def user_id
|
58
|
+
session[:user_id]
|
59
|
+
end
|
60
|
+
|
61
|
+
def tool_consumer_instance_guid
|
62
|
+
session[:tool_consumer_instance_guid]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module CanvasInteractor
|
2
|
+
module CanvasConfig
|
3
|
+
mattr_accessor :key, :secret
|
4
|
+
|
5
|
+
def self.load_config
|
6
|
+
YAML::load(File.open(config_file))[Rails.env]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.config_file
|
10
|
+
CanvasOauth.app_root.join('config/canvas.yml')
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.setup!
|
14
|
+
if File.exists?(config_file)
|
15
|
+
Rails.logger.info "Initializing Canvas using configuration in #{config_file}"
|
16
|
+
config = load_config
|
17
|
+
self.key = config['key']
|
18
|
+
self.secret = config['secret']
|
19
|
+
elsif ENV['CANVAS_KEY'].present? && ENV['CANVAS_SECRET'].present?
|
20
|
+
Rails.logger.info "Initializing Canvas using environment vars CANVAS_KEY and CANVAS_SECRET"
|
21
|
+
self.key = ENV['CANVAS_KEY']
|
22
|
+
self.secret = ENV['CANVAS_SECRET']
|
23
|
+
else
|
24
|
+
raise "Warning: Canvas key and secret not configured (RAILS_ENV = #{ENV['RAILS_ENV']})."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: canvas_interactor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ahmad Hassan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-06-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: httparty
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.9.0
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.9.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: link_header
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.0.6
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.0.6
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pg
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: sqlite3
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rspec-its
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rspec-rails
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: rspec-rails-mocha
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: shoulda-matchers
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: webmock
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: guard-rspec
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
type: :development
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
- !ruby/object:Gem::Dependency
|
188
|
+
name: rb-fsevent
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
type: :development
|
195
|
+
prerelease: false
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
description: Canvas Interactor is a mountable engine for handling the oauth workflow
|
202
|
+
with canvas and making api calls from your rails app.
|
203
|
+
email:
|
204
|
+
- er.ahmad.hassan@gmail.com
|
205
|
+
executables: []
|
206
|
+
extensions: []
|
207
|
+
extra_rdoc_files: []
|
208
|
+
files:
|
209
|
+
- MIT-LICENSE
|
210
|
+
- README.md
|
211
|
+
- Rakefile
|
212
|
+
- app/assets/config/canvas_interactor_manifest.js
|
213
|
+
- app/assets/javascripts/canvas_interactor/application.js
|
214
|
+
- app/assets/stylesheets/canvas_interactor/application.css
|
215
|
+
- app/controllers/canvas_interactor/application_controller.rb
|
216
|
+
- app/controllers/canvas_interactor/canvas_controller.rb
|
217
|
+
- app/helpers/canvas_interactor/application_helper.rb
|
218
|
+
- app/jobs/canvas_interactor/application_job.rb
|
219
|
+
- app/mailers/canvas_interactor/application_mailer.rb
|
220
|
+
- app/models/canvas_interactor/application_record.rb
|
221
|
+
- app/models/canvas_interactor/authorization.rb
|
222
|
+
- app/views/layouts/canvas_interactor/application.html.erb
|
223
|
+
- config/routes.rb
|
224
|
+
- db/migrate/20180607153145_create_canvas_interactor_authorizations.rb
|
225
|
+
- lib/canvas_interactor.rb
|
226
|
+
- lib/canvas_interactor/canvas_api.rb
|
227
|
+
- lib/canvas_interactor/canvas_api_extensions.rb
|
228
|
+
- lib/canvas_interactor/canvas_application.rb
|
229
|
+
- lib/canvas_interactor/canvas_config.rb
|
230
|
+
- lib/canvas_interactor/config.rb
|
231
|
+
- lib/canvas_interactor/engine.rb
|
232
|
+
- lib/canvas_interactor/version.rb
|
233
|
+
- lib/tasks/canvas_interactor_tasks.rake
|
234
|
+
homepage: https://github.com/ahmadhasankhan/canvas_interactor
|
235
|
+
licenses:
|
236
|
+
- MIT
|
237
|
+
metadata: {}
|
238
|
+
post_install_message:
|
239
|
+
rdoc_options: []
|
240
|
+
require_paths:
|
241
|
+
- lib
|
242
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
243
|
+
requirements:
|
244
|
+
- - ">="
|
245
|
+
- !ruby/object:Gem::Version
|
246
|
+
version: '0'
|
247
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
248
|
+
requirements:
|
249
|
+
- - ">="
|
250
|
+
- !ruby/object:Gem::Version
|
251
|
+
version: '0'
|
252
|
+
requirements: []
|
253
|
+
rubyforge_project:
|
254
|
+
rubygems_version: 2.7.6
|
255
|
+
signing_key:
|
256
|
+
specification_version: 4
|
257
|
+
summary: Canvas Interactor is a mountable engine for handling the oauth workflow with
|
258
|
+
canvas and making api calls from your rails app.
|
259
|
+
test_files: []
|