Skip to content

Commit 77b68d3

Browse files
authored
Merge pull request #453 from macite/new/d2lintegration
New D2L Integration
2 parents 3ff0c5d + 33a53f7 commit 77b68d3

37 files changed

+1391
-64
lines changed

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ gem 'tca_client', '1.0.4'
103103
# Async jobs
104104
gem 'sidekiq'
105105
gem 'sidekiq-cron'
106+
gem 'sidekiq-unique-jobs'
106107

107108
# Redis for sidekiq, caching, and action cable (eventually)
108109
gem 'redis'
@@ -112,3 +113,6 @@ gem 'shellwords'
112113

113114
# PDF reader for validating PDF file submissions
114115
gem 'pdf-reader'
116+
117+
# oauth gem for OAuth2 authentication - D2L
118+
gem 'oauth2'

Gemfile.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ GEM
190190
railties (>= 6.0.6.1)
191191
hashdiff (1.1.0)
192192
hashery (2.1.2)
193+
hashie (5.0.0)
193194
hirb (0.7.3)
194195
http-accept (1.7.0)
195196
http-cookie (1.0.6)
@@ -212,6 +213,8 @@ GEM
212213
bindata
213214
faraday (~> 2.0)
214215
faraday-follow_redirects
216+
jwt (2.9.3)
217+
base64
215218
kramdown (2.4.0)
216219
rexml
217220
kramdown-parser-gfm (1.1.0)
@@ -243,6 +246,8 @@ GEM
243246
tcp_timeout (~> 0.1.1)
244247
msgpack (1.7.2)
245248
multi_json (1.15.0)
249+
multi_xml (0.7.1)
250+
bigdecimal (~> 3.1)
246251
mustermann (3.0.0)
247252
ruby2_keywords (~> 0.0.1)
248253
mustermann-grape (1.1.0)
@@ -267,6 +272,13 @@ GEM
267272
racc (~> 1.4)
268273
nokogiri (1.16.6-x86_64-linux)
269274
racc (~> 1.4)
275+
oauth2 (2.0.9)
276+
faraday (>= 0.17.3, < 3.0)
277+
jwt (>= 1.0, < 3.0)
278+
multi_xml (~> 0.5)
279+
rack (>= 1.2, < 4)
280+
snaky_hash (~> 2.0)
281+
version_gem (~> 1.1)
270282
observer (0.1.2)
271283
orm_adapter (0.5.0)
272284
parallel (1.25.1)
@@ -431,12 +443,19 @@ GEM
431443
fugit (~> 1.8)
432444
globalid (>= 1.0.1)
433445
sidekiq (>= 6)
446+
sidekiq-unique-jobs (8.0.10)
447+
concurrent-ruby (~> 1.0, >= 1.0.5)
448+
sidekiq (>= 7.0.0, < 8.0.0)
449+
thor (>= 1.0, < 3.0)
434450
simplecov (0.22.0)
435451
docile (~> 1.1)
436452
simplecov-html (~> 0.11)
437453
simplecov_json_formatter (~> 0.1)
438454
simplecov-html (0.12.3)
439455
simplecov_json_formatter (0.1.4)
456+
snaky_hash (2.0.1)
457+
hashie
458+
version_gem (~> 1.1, >= 1.1.1)
440459
solargraph (0.50.0)
441460
backport (~> 1.2)
442461
benchmark
@@ -483,6 +502,7 @@ GEM
483502
concurrent-ruby (~> 1.0)
484503
unicode-display_width (2.5.0)
485504
uri (0.13.0)
505+
version_gem (1.1.4)
486506
warden (1.2.9)
487507
rack (>= 2.0.9)
488508
webmock (3.23.1)
@@ -528,6 +548,7 @@ DEPENDENCIES
528548
moss_ruby (>= 1.1.4)
529549
mysql2
530550
net-smtp
551+
oauth2
531552
pdf-reader
532553
puma
533554
rack-cors
@@ -553,6 +574,7 @@ DEPENDENCIES
553574
shellwords
554575
sidekiq
555576
sidekiq-cron
577+
sidekiq-unique-jobs
556578
simplecov
557579
solargraph
558580
sprockets-rails

README.md

Lines changed: 37 additions & 18 deletions
Large diffs are not rendered by default.

app/api/api_root.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ class ApiRoot < Grape::API
9191
mount TutorialEnrolmentsApi
9292
mount UnitRolesApi
9393
mount UnitsApi
94+
95+
mount D2lIntegrationApi::D2lApi
96+
mount D2lIntegrationApi::OauthPublicApi
97+
9498
mount UsersApi
9599
mount WebcalApi
96100
mount WebcalPublicApi
@@ -134,6 +138,8 @@ class ApiRoot < Grape::API
134138
AuthenticationHelpers.add_auth_to ScormApi
135139
AuthenticationHelpers.add_auth_to TestAttemptsApi
136140

141+
AuthenticationHelpers.add_auth_to D2lIntegrationApi::D2lApi
142+
137143
add_swagger_documentation \
138144
base_path: nil,
139145
api_version: 'v1',

app/api/authentication_api.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class AuthenticationApi < Grape::API
147147
protocol = Rails.env.development? ? 'http' : 'https'
148148
host = "#{protocol}://#{host}"
149149
end
150-
redirect "#{host}/#/sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}"
150+
redirect "#{host}/sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}"
151151
end
152152
end
153153

@@ -224,7 +224,7 @@ class AuthenticationApi < Grape::API
224224
protocol = Rails.env.development? ? 'http' : 'https'
225225
host = "#{protocol}://#{host}"
226226
end
227-
redirect "#{host}/#/sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}"
227+
redirect "#{host}/sign_in?authToken=#{onetime_token.authentication_token}&username=#{user.username}"
228228
end
229229
end
230230

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
require 'grape'
2+
3+
module D2lIntegrationApi
4+
# The D2l API provides the frontend with the ability to register
5+
# integration details to connect units with D2L. This will allow
6+
# grade book items to be copied from portfolio results to D2L.
7+
class D2lApi < Grape::API
8+
helpers AuthenticationHelpers
9+
helpers AuthorisationHelpers
10+
helpers FileStreamHelper
11+
include LogHelper
12+
13+
before do
14+
authenticated?
15+
end
16+
17+
desc 'Get the D2L assessment mapping for a unit'
18+
get '/units/:unit_id/d2l' do
19+
unit = Unit.find(params[:unit_id])
20+
21+
unless authorise?(current_user, unit, :update)
22+
error!({ error: 'Not authorised to view D2L details' }, 403)
23+
end
24+
25+
present unit.d2l_assessment_mapping, with: D2lIntegrationApi::Entities::D2lEntity
26+
end
27+
28+
desc 'Create a D2L assessment mapping for a unit'
29+
params do
30+
requires :org_unit_id, type: String, desc: 'The org unit id for the D2L unit'
31+
optional :grade_object_id, type: Numeric, desc: 'The grade object id for the D2L unit'
32+
end
33+
post '/units/:unit_id/d2l' do
34+
unit = Unit.find(params[:unit_id])
35+
36+
unless authorise?(current_user, unit, :update)
37+
error!({ error: 'Not authorised to add D2L details' }, 403)
38+
end
39+
40+
accepted_params = ActionController::Parameters.new(params).permit(:unit_id, :org_unit_id, :grade_object_id)
41+
42+
d2l = D2lAssessmentMapping.create!(accepted_params)
43+
present d2l, with: D2lIntegrationApi::Entities::D2lEntity
44+
end
45+
46+
desc 'Delete a D2L assessment mapping for a unit'
47+
delete '/units/:unit_id/d2l/:id' do
48+
unit = Unit.find(params[:unit_id])
49+
50+
unless authorise?(current_user, unit, :update)
51+
error!({ error: 'Not authorised to delete D2L details' }, 403)
52+
end
53+
54+
d2l = unit.d2l_assessment_mapping
55+
56+
if d2l.id != params[:id].to_i
57+
error!({ error: 'D2L details not found' }, 404)
58+
end
59+
60+
d2l.destroy if d2l.present?
61+
status 204
62+
end
63+
64+
desc 'Update a D2L assessment mapping for a unit'
65+
params do
66+
optional :org_unit_id, type: String, desc: 'The org unit id for the D2L unit'
67+
optional :grade_object_id, type: Numeric, desc: 'The grade object id for the D2L unit'
68+
end
69+
put '/units/:unit_id/d2l/:id' do
70+
unit = Unit.find(params[:unit_id])
71+
72+
unless authorise?(current_user, unit, :update)
73+
error!({ error: 'Not authorised to update D2L details' }, 403)
74+
end
75+
76+
d2l = unit.d2l_assessment_mapping
77+
78+
if d2l.id != params[:id].to_i
79+
error!({ error: 'D2L details not found' }, 404)
80+
end
81+
82+
accepted_params = ActionController::Parameters.new(params).permit(:org_unit_id, :grade_object_id)
83+
84+
d2l.update!(accepted_params)
85+
present d2l, with: D2lIntegrationApi::Entities::D2lEntity
86+
end
87+
88+
desc 'Initiate a login to D2L as a convenor or admin'
89+
post '/d2l/login_url' do
90+
unless authorise? current_user, User, :convene_units
91+
error!({ error: 'Not authorised to login to D2L' }, 403)
92+
end
93+
94+
begin
95+
response = D2lIntegration.login_url(current_user)
96+
rescue StandardError => e
97+
error!({ error: e.message }, 500)
98+
end
99+
100+
present response, with: Grape::Presenters::Presenter
101+
end
102+
103+
desc 'Trigger the posting of grades to D2L'
104+
post '/units/:unit_id/d2l/grades' do
105+
unit = Unit.find(params[:unit_id])
106+
107+
unless authorise?(current_user, unit, :update)
108+
error!({ error: 'Not authorised to post grades to D2L' }, 403)
109+
end
110+
111+
if unit.d2l_assessment_mapping.blank?
112+
error!({ error: 'Configure D2L details for unit before starting transfer' }, 403)
113+
end
114+
115+
token = current_user.user_oauth_tokens.where(provider: :d2l).last
116+
if token.blank? || token.expires_at < 10.minutes.from_now
117+
error!({ error: 'Login to D2L before transferring results' }, 403)
118+
end
119+
120+
D2lPostGradesJob.perform_async(unit.id, current_user.id)
121+
122+
status 202
123+
end
124+
125+
desc 'Get the result of a grade transfer to D2L'
126+
get '/units/:unit_id/d2l/grades' do
127+
unit = Unit.find(params[:unit_id])
128+
129+
unless authorise?(current_user, unit, :update)
130+
error!({ error: 'Not authorised to view grade transfer results' }, 403)
131+
end
132+
133+
file_path = D2lIntegration.result_file_path(unit)
134+
unless File.exist?(file_path)
135+
error!({ error: 'No grade transfer result found' }, 404)
136+
end
137+
138+
content_type 'text/csv'
139+
140+
stream_file(file_path)
141+
end
142+
143+
desc 'Determing if grade results are available for a unit'
144+
get '/units/:unit_id/d2l/grades/available' do
145+
unit = Unit.find(params[:unit_id])
146+
147+
unless authorise?(current_user, unit, :update)
148+
error!({ error: 'Not authorised to view grade transfer results' }, 403)
149+
end
150+
151+
file_path = D2lIntegration.result_file_path(unit)
152+
response = {
153+
available: File.exist?(file_path),
154+
running: D2lIntegration.d2l_grade_job_present?(unit)
155+
}
156+
157+
present response, with: Grape::Presenters::Presenter
158+
end
159+
160+
desc 'Determing if unit is weighted'
161+
get '/units/:unit_id/d2l/grades/weighted' do
162+
unit = Unit.find(params[:unit_id])
163+
164+
unless authorise?(current_user, unit, :update)
165+
error!({ error: 'Not authorised to view unit details' }, 403)
166+
end
167+
168+
d2l = unit.d2l_assessment_mapping
169+
170+
return false unless d2l.present? && d2l.org_unit_id.present?
171+
172+
present D2lIntegration.grade_weighted?(d2l, current_user), with: Grape::Presenters::Presenter
173+
end
174+
175+
desc 'Get D2L api endpoint'
176+
get '/d2l/endpoint' do
177+
unless authorise? current_user, User, :convene_units
178+
error!({ error: 'Not authorised to view D2L endpoint' }, 403)
179+
end
180+
181+
present D2lIntegration.d2l_api_host, with: Grape::Presenters::Presenter
182+
end
183+
end
184+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module D2lIntegrationApi
2+
module Entities
3+
class D2lEntity < Grape::Entity
4+
expose :id
5+
expose :org_unit_id
6+
expose :grade_object_id
7+
end
8+
end
9+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'grape'
2+
3+
module D2lIntegrationApi
4+
# Public api for oauth callback
5+
class OauthPublicApi < Grape::API
6+
include LogHelper
7+
8+
desc 'Callback for oauth login'
9+
params do
10+
requires :code, type: String, desc: 'The code returned from the OAuth login'
11+
requires :state, type: String, desc: 'The state returned from the OAuth login'
12+
end
13+
get '/d2l/callback' do
14+
D2lIntegration.process_callback(params[:code], params[:state])
15+
16+
host = Doubtfire::Application.config.institution[:host]
17+
redirect "#{host}/success-close"
18+
rescue StandardError => e
19+
error!({ error: "Error processing oauth callback: #{e.message}" }, 500)
20+
end
21+
end
22+
end

app/api/settings_api.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class SettingsApi < Grape::API
99
response = {
1010
externalName: Doubtfire::Application.config.institution[:product_name],
1111
overseerEnabled: Doubtfire::Application.config.overseer_enabled,
12-
tiiEnabled: TurnItIn.enabled?
12+
tiiEnabled: TurnItIn.enabled?,
13+
d2lEnabled: D2lIntegration.enabled?
1314
}
1415

1516
present response, with: Grape::Presenters::Presenter

0 commit comments

Comments
 (0)