d2l_sdk 0.1.7
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/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +204 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/d2l_sdk.gemspec +42 -0
- data/example_scripts/adjusted_courses.txt +44446 -0
- data/example_scripts/fix_201708_courses.rb +13 -0
- data/example_scripts/update_enddates.rb +88 -0
- data/lib/d2l_sdk.rb +16 -0
- data/lib/d2l_sdk/auth.rb +95 -0
- data/lib/d2l_sdk/config.rb +56 -0
- data/lib/d2l_sdk/config_variables.rb +81 -0
- data/lib/d2l_sdk/course.rb +311 -0
- data/lib/d2l_sdk/course_content.rb +362 -0
- data/lib/d2l_sdk/course_template.rb +179 -0
- data/lib/d2l_sdk/datahub.rb +213 -0
- data/lib/d2l_sdk/demographics.rb +89 -0
- data/lib/d2l_sdk/enroll.rb +133 -0
- data/lib/d2l_sdk/group.rb +232 -0
- data/lib/d2l_sdk/logging.rb +57 -0
- data/lib/d2l_sdk/news.rb +20 -0
- data/lib/d2l_sdk/org_unit.rb +383 -0
- data/lib/d2l_sdk/requests.rb +241 -0
- data/lib/d2l_sdk/section.rb +207 -0
- data/lib/d2l_sdk/semester.rb +159 -0
- data/lib/d2l_sdk/user.rb +420 -0
- data/lib/d2l_sdk/version.rb +3 -0
- metadata +228 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "lib/d2l_sdk"
|
|
2
|
+
|
|
3
|
+
# ap get_all_semesters.reject{|semester| !semester["Code"].include?(201708.to_s)}
|
|
4
|
+
semester_201708_id = "111723" # Fall '17 semester recieved through line of code above
|
|
5
|
+
courses = get_courses_by_code(201708) # get all courses coded as 201708
|
|
6
|
+
courses_done = 0
|
|
7
|
+
courses.each do |course| # for each of these 201708 courses
|
|
8
|
+
# print out progress thus far
|
|
9
|
+
puts "Progress: #{courses_done}/#{courses.length} (#{(courses_done/courses.length).to_i})" if courses_done % 10 == 0
|
|
10
|
+
course_id = course["Identifier"] # get their identifier
|
|
11
|
+
add_parent_to_org_unit(semester_201708_id, course_id) # add the semester as the parent
|
|
12
|
+
courses_done += 1
|
|
13
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require_relative "lib/d2l_sdk"
|
|
2
|
+
require "set"
|
|
3
|
+
|
|
4
|
+
@debug = false
|
|
5
|
+
@testing = false
|
|
6
|
+
start = Time.now
|
|
7
|
+
puts "Getting all courses"
|
|
8
|
+
all_courses = get_all_courses
|
|
9
|
+
duration = Time.now - start
|
|
10
|
+
puts "#{all_courses.size} courses retrieved. Time taken: #{Time.at(duration).strftime "%M:%S"}"
|
|
11
|
+
iteration = 0 #if @testing
|
|
12
|
+
max_iterations = 10 if @testing
|
|
13
|
+
start = Time.now
|
|
14
|
+
|
|
15
|
+
def get_adjusted_courses(file_path)
|
|
16
|
+
# If there isnt already a file containing all adjusted courses, create one
|
|
17
|
+
File.write(file_path, '') {} unless File.exist?(file_path)
|
|
18
|
+
puts "Grabbing adjusted courses from file at: #{file_path}"
|
|
19
|
+
adjusted_courses = Set.new [] # assures unique student ids // none processed >1 times
|
|
20
|
+
File.open(file_path, 'r') do |file_handle|
|
|
21
|
+
file_handle.each_line do |line|
|
|
22
|
+
adjusted_courses.add(line.gsub(/\n/,''))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
puts "adjusted courses pulled from file and stored in array 'adjusted_courses'"
|
|
26
|
+
adjusted_courses # Returns set of adjusted course OU IDs (would ruin time complexity with an array)
|
|
27
|
+
end
|
|
28
|
+
file_path = "adjusted_courses.txt"
|
|
29
|
+
adjusted_courses = get_adjusted_courses(file_path)
|
|
30
|
+
year_ago = Time.new(2016)
|
|
31
|
+
all_courses.each do |course_arr_item|
|
|
32
|
+
|
|
33
|
+
# testing constraints to limit number of iterations.
|
|
34
|
+
iteration += 1
|
|
35
|
+
if @testing
|
|
36
|
+
break if iteration >= max_iterations
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# append to a file (asserted that its already created as get_adjusted_courses handles this.)
|
|
40
|
+
course_id = course_arr_item["Identifier"]
|
|
41
|
+
if adjusted_courses.include?(course_id)
|
|
42
|
+
puts "Course [#{course_id}] already has been adjusted." if @debug
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
if course_arr_item["Identifier"].nil?
|
|
46
|
+
ap course_arr_item
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
# Retrieve course data by ID
|
|
50
|
+
course = get_course_by_id(course_id)
|
|
51
|
+
puts "Original course information returned by its id:" if @debug
|
|
52
|
+
File.open(file_path, 'a'){ |f| f << "#{course_id}\n"}
|
|
53
|
+
puts "Course [#{course["Name"]} (#{course_id})] has been added to #{file_path}" if @debug
|
|
54
|
+
|
|
55
|
+
ap course if @debug
|
|
56
|
+
new_end_date = course["EndDate"]
|
|
57
|
+
# if course endDate is nil, print a message and skip it
|
|
58
|
+
if new_end_date.nil?
|
|
59
|
+
puts "Course [#{course["Name"]} (#{course_id})] does not have a valid endDate (reason: nil)" if @debug
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
puts "[#{iteration}/#{all_courses.size}]Course [#{course["Name"]} (#{course_id})] original end date: #{new_end_date}"# if @debug
|
|
63
|
+
new_end_date = Time.parse(new_end_date) + (60 * 60 * 24 * 7) # Seconds * Minutes * Hours * Days
|
|
64
|
+
if new_end_date < year_ago # if before 2016...
|
|
65
|
+
puts "Course [#{course["Name"]} (#{course_id})] is not within the filtered set of classes (reason: before 2016)" if @debug
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
new_end_date = new_end_date.iso8601(3)
|
|
69
|
+
|
|
70
|
+
update_payload = {
|
|
71
|
+
'Name' => course["Name"], # String
|
|
72
|
+
'Code' => course["Code"], # String
|
|
73
|
+
'StartDate' => course["StartDate"], # String: UTCDateTime | nil
|
|
74
|
+
'EndDate' => "#{new_end_date}", # String: UTCDateTime | nil
|
|
75
|
+
'IsActive' => course["IsActive"] # bool
|
|
76
|
+
}
|
|
77
|
+
ap update_payload if @debug
|
|
78
|
+
update_course_data(course_id, update_payload)
|
|
79
|
+
updated_course = get_course_by_id(course_id)
|
|
80
|
+
puts "Course [#{updated_course["Name"]} (#{course_id})] adjusted end date: #{updated_course["EndDate"]}"
|
|
81
|
+
|
|
82
|
+
puts "Updated course information returned by its id:" if @debug
|
|
83
|
+
ap get_course_by_id(course_id) if @debug
|
|
84
|
+
end
|
|
85
|
+
#ap adjusted_courses.to_a
|
|
86
|
+
#print "[!] duration of 5000 iterations, where 3000 have already been completed: "
|
|
87
|
+
duration = Time.now - start
|
|
88
|
+
puts Time.at(duration).strftime "%M:%S"
|
data/lib/d2l_sdk.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require_relative 'd2l_sdk/course_template'
|
|
2
|
+
require_relative 'd2l_sdk/course'
|
|
3
|
+
require_relative 'd2l_sdk/datahub'
|
|
4
|
+
require_relative 'd2l_sdk/enroll'
|
|
5
|
+
require_relative 'd2l_sdk/group'
|
|
6
|
+
require_relative 'd2l_sdk/org_unit'
|
|
7
|
+
require_relative 'd2l_sdk/section'
|
|
8
|
+
require_relative 'd2l_sdk/semester'
|
|
9
|
+
require_relative 'd2l_sdk/user'
|
|
10
|
+
require_relative 'd2l_sdk/config_variables'
|
|
11
|
+
require_relative 'd2l_sdk/demographics'
|
|
12
|
+
require_relative 'd2l_sdk/logging'
|
|
13
|
+
require_relative 'd2l_sdk/course_content'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
puts "d2l_sdk loaded"
|
data/lib/d2l_sdk/auth.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require 'rubygems' # useful
|
|
2
|
+
require 'awesome_print' # useful for debugging
|
|
3
|
+
require 'base64' # NEEDED
|
|
4
|
+
require 'json' # NEEDED
|
|
5
|
+
require 'restclient' # NEEDED
|
|
6
|
+
require 'openssl' # NEEDED
|
|
7
|
+
require 'open-uri' # NEEDED
|
|
8
|
+
require 'colorize' # useful
|
|
9
|
+
require_relative 'config'
|
|
10
|
+
|
|
11
|
+
# Global variables initialized through require_relative 'D2L_Config'
|
|
12
|
+
|
|
13
|
+
# requests input from the user, cuts off any new line and downcases it.
|
|
14
|
+
#
|
|
15
|
+
# returns: String::downcased_user_input
|
|
16
|
+
def prompt(*args)
|
|
17
|
+
print(*args)
|
|
18
|
+
gets.chomp.downcase
|
|
19
|
+
# returns: String::downcased_user_input
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Creates an authenticated uniform resource identifier that works with Valence
|
|
23
|
+
# by calling +URI.parse+ using the path downcased, then creating a query string
|
|
24
|
+
# by calling +_get_string+ with the parsed_url and the http_method. These are
|
|
25
|
+
# used as the Variables and then returned as the finished uri.
|
|
26
|
+
#
|
|
27
|
+
# Input that is required is:
|
|
28
|
+
# * path: The path to the resource you are trying to accessing
|
|
29
|
+
# * http_method: The method utilized to access/modify the resource
|
|
30
|
+
#
|
|
31
|
+
# returns: String::uri
|
|
32
|
+
def create_authenticated_uri(path, http_method)
|
|
33
|
+
parsed_url = URI.parse(path.downcase)
|
|
34
|
+
uri_scheme = 'https'
|
|
35
|
+
query_string = _get_string(parsed_url.path, http_method)
|
|
36
|
+
uri = uri_scheme + '://' + $hostname + parsed_url.path + query_string
|
|
37
|
+
uri << '&' + parsed_url.query if parsed_url.query
|
|
38
|
+
uri
|
|
39
|
+
# returns: String::uri
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Builds an authenticated uniform resource identifier query string that
|
|
43
|
+
# works properly with the Valence API.
|
|
44
|
+
#
|
|
45
|
+
# Required Variables:
|
|
46
|
+
# * app_id, user_id, app_key, user_key
|
|
47
|
+
#
|
|
48
|
+
# returns: String::'authenticated_uri'
|
|
49
|
+
def build_authenticated_uri_query_string(signature, timestamp)
|
|
50
|
+
"?x_a=#{$app_id}"\
|
|
51
|
+
"&x_b=#{$user_id}"\
|
|
52
|
+
"&x_c=#{get_base64_hash_string($app_key, signature)}"\
|
|
53
|
+
"&x_d=#{get_base64_hash_string($user_key, signature)}"\
|
|
54
|
+
"&x_t=#{timestamp}"
|
|
55
|
+
# returns: String::'authenticated_uri'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# uses the path, http_method, and timestamp arguments to create a properly
|
|
59
|
+
# formatted signature. Then, this is returned.
|
|
60
|
+
#
|
|
61
|
+
# returns: String::signature
|
|
62
|
+
def format_signature(path, http_method, timestamp)
|
|
63
|
+
http_method.upcase + '&' + path.encode('UTF-8') + '&' + timestamp.to_s
|
|
64
|
+
# returns: String::signature
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# uses the key and signature as arguments to create a hash using
|
|
68
|
+
# +OpenSSL::HMAC.digest+ with an additional argument denoting the hashing
|
|
69
|
+
# algorithm as 'sha256'. The hash is then encoded properly and all "="
|
|
70
|
+
# are deleted to officially create a base64 hash string.
|
|
71
|
+
#
|
|
72
|
+
# returns: String::base64_hash_string
|
|
73
|
+
def get_base64_hash_string(key, signature)
|
|
74
|
+
hash = OpenSSL::HMAC.digest('sha256', key, signature)
|
|
75
|
+
Base64.urlsafe_encode64(hash).delete('=')
|
|
76
|
+
# returns: String::base64_hash_string
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Used as a helper method for create_authenticated_uri in order to properly
|
|
80
|
+
# create a query string that will (hopefully) work with the Valence API.
|
|
81
|
+
# the arguments path and http_method are used as arguments with the current time
|
|
82
|
+
# for +format_signature+ and +build_authenticated_uri_query_string+.
|
|
83
|
+
#
|
|
84
|
+
# returns: String::query_string
|
|
85
|
+
def _get_string(path, http_method)
|
|
86
|
+
timestamp = Time.now.to_i
|
|
87
|
+
signature = format_signature(path, http_method, timestamp)
|
|
88
|
+
unless path.include? "/auth/api/token"
|
|
89
|
+
build_authenticated_uri_query_string(signature, timestamp)
|
|
90
|
+
else
|
|
91
|
+
# build authenticated query string not using typical schema
|
|
92
|
+
build_authenticated_token_uri_query_string(signature, timestamp)
|
|
93
|
+
end
|
|
94
|
+
# returns: String::query_string
|
|
95
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#Is this the production environment?
|
|
2
|
+
require 'json'
|
|
3
|
+
@production = true
|
|
4
|
+
@debug = true
|
|
5
|
+
# Conditional on whether this is production or test server
|
|
6
|
+
config_file_name = 'd2l_config.json'
|
|
7
|
+
# If a configuration file already exists...
|
|
8
|
+
if File.exist?(config_file_name)
|
|
9
|
+
config_file = File.read(config_file_name)
|
|
10
|
+
config = JSON.parse(config_file)
|
|
11
|
+
puts "[+] Configuration Variables:" if @debug
|
|
12
|
+
puts "[-] hostname: #{config["hostname"]}" if @debug
|
|
13
|
+
$hostname = config["hostname"]
|
|
14
|
+
puts "[-] user_id: #{config["user_id"]}" if @debug
|
|
15
|
+
$user_id = config["user_id"]
|
|
16
|
+
puts "[-] user_key: #{config["user_key"]}" if @debug
|
|
17
|
+
$user_key = config["user_key"]
|
|
18
|
+
puts "[-] app_id: #{config["app_id"]}" if @debug
|
|
19
|
+
$app_id = config["app_id"]
|
|
20
|
+
puts "[-] app_key: #{config["app_key"]}" if @debug
|
|
21
|
+
$app_key = config["app_key"]
|
|
22
|
+
# else if a configuration file doesnt exist, create one and load the config vars!
|
|
23
|
+
else
|
|
24
|
+
puts "[!] No file by the name 'd2l_config.json' found!"
|
|
25
|
+
puts "[-] Initializing 'd2l_config.json' in current directory..\n"\
|
|
26
|
+
" Please enter the following information..."
|
|
27
|
+
# host of D2L server
|
|
28
|
+
print "hostname: "
|
|
29
|
+
$hostname = gets.chomp.gsub(/'|\"|https:\/\/|http:\/\/|/,'').strip
|
|
30
|
+
# api-user id
|
|
31
|
+
print "user_id: "
|
|
32
|
+
$user_id = gets.chomp.gsub(/'|\"/,'').strip
|
|
33
|
+
# api-user key
|
|
34
|
+
print "user_key: "
|
|
35
|
+
$user_key = gets.chomp.gsub(/'|\"/,'').strip
|
|
36
|
+
# app id (received from apitesttool)
|
|
37
|
+
print "app_id: "
|
|
38
|
+
$app_id = gets.chomp.gsub(/'|\"/,'').strip
|
|
39
|
+
# app key (same as app id retrieval)
|
|
40
|
+
print "app_key: "
|
|
41
|
+
$app_key = gets.chomp.gsub(/'|\"/,'').strip
|
|
42
|
+
|
|
43
|
+
config_hash = {
|
|
44
|
+
"hostname" => $hostname,
|
|
45
|
+
"user_id" => $user_id,
|
|
46
|
+
"user_key" => $user_key,
|
|
47
|
+
"app_id" => $app_id,
|
|
48
|
+
"app_key" => $app_key
|
|
49
|
+
}
|
|
50
|
+
json = JSON.pretty_generate(config_hash)
|
|
51
|
+
puts json if @debug
|
|
52
|
+
#puts JSON.parse(json)["hostname"] if @debug
|
|
53
|
+
config = File.new("d2l_config.json","w")
|
|
54
|
+
config.puts(json)
|
|
55
|
+
config.close
|
|
56
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require_relative 'auth'
|
|
2
|
+
|
|
3
|
+
########################
|
|
4
|
+
# CONFIG VARIABLES:#####
|
|
5
|
+
########################
|
|
6
|
+
@debug = false
|
|
7
|
+
|
|
8
|
+
#Retrieve the definitions for all the configuration variables the user has access to view.
|
|
9
|
+
def get_all_config_var_definitions(search='', bookmark='')
|
|
10
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/definitions/"
|
|
11
|
+
path += "?search=#{search}" if search != ''
|
|
12
|
+
path += "?bookmark=#{bookmark}" if bookmark != ''
|
|
13
|
+
_get(path)
|
|
14
|
+
#returns paged result set of Definition JSON data blocks
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#Retrieve the definitions for a configuration variable.
|
|
18
|
+
def get_config_var_definitions(variable_id)
|
|
19
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/(#{variable_id}/definition"
|
|
20
|
+
_get(path)
|
|
21
|
+
# returns Definition JSON data block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#Retrieve the value summary for a configuration variable.
|
|
25
|
+
def get_config_var_values(variable_id)
|
|
26
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values"
|
|
27
|
+
_get(path)
|
|
28
|
+
# returns Values JSON data block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#Retrieve the current org value for a configuration variable.
|
|
32
|
+
def get_config_var_current_org_value(variable_id)
|
|
33
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values/org"
|
|
34
|
+
_get(path)
|
|
35
|
+
# returns OrgValue JSON data block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Retrieve all the org unit override values for a configuration variable.
|
|
39
|
+
def get_all_config_var_org_unit_override_values(variable_id, bookmark='')
|
|
40
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values/orgUnits/"
|
|
41
|
+
path += "?bookmark=#{bookmark}" if bookmark != ''
|
|
42
|
+
_get(path)
|
|
43
|
+
# returns paged result set of OrgUnitValue data blocks
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Retrieve an org unit override value for a configuration variable.
|
|
47
|
+
def get_config_var_org_unit_override_value(variable_id, org_unit_id)
|
|
48
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values/orgUnits/#{org_unit_id}"
|
|
49
|
+
_get(path)
|
|
50
|
+
# returns OrgUnitValue JSON block
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Retrieve the effective value for a configuration variable within an org unit.
|
|
54
|
+
def get_config_var_org_unit_effective_value(variable_id, org_unit_id)
|
|
55
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/effectiveValues/orgUnits/#{org_unit_id}"
|
|
56
|
+
_get(path)
|
|
57
|
+
# returns OrgUnitValue JSON block
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Retrieve all the role override values for a configuration variable.
|
|
61
|
+
def get_all_config_var_org_unit_role_override_values(variable_id, bookmark='')
|
|
62
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values/roles/"
|
|
63
|
+
path += "?bookmark=#{bookmark}" if bookmark != ''
|
|
64
|
+
_get(path)
|
|
65
|
+
# returns paged result set with RoleValue JSON data blocks
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def get_config_var_role_override_value(variable_id, role_id)
|
|
69
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values/roles/#{role_id}"
|
|
70
|
+
_get(path)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_config_var_system_value(variable_id)
|
|
74
|
+
path = "/d2l/api/lp/#{$lp_ver}/configVariables/#{variable_id}/values/system"
|
|
75
|
+
_get(path)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_config_var_resolver(variable_id)
|
|
79
|
+
path = "/d2l/api/lp/#{lp_ver}/configVariables/#{variable_id}/resolver"
|
|
80
|
+
_get(path)
|
|
81
|
+
end
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
require_relative 'requests'
|
|
2
|
+
require 'json-schema'
|
|
3
|
+
########################
|
|
4
|
+
# COURSES:##############
|
|
5
|
+
########################
|
|
6
|
+
|
|
7
|
+
# Checks whether the created course data conforms to the valence api for the
|
|
8
|
+
# course data JSON object. If it does conform, then nothing happens and it
|
|
9
|
+
# simply returns true. If it does not conform, then the JSON validator raises
|
|
10
|
+
# an exception.
|
|
11
|
+
def check_course_data_validity(course_data)
|
|
12
|
+
schema = {
|
|
13
|
+
'type' => 'object',
|
|
14
|
+
'required' => %w(Name Code CourseTemplateId SemesterId
|
|
15
|
+
StartDate EndDate LocaleId ForceLocale
|
|
16
|
+
ShowAddressBook),
|
|
17
|
+
'properties' => {
|
|
18
|
+
'Name' => { 'type' => 'string' },
|
|
19
|
+
'Code' => { 'type' => 'string' },
|
|
20
|
+
'CourseTemplateId' => { 'type' => 'integer' },
|
|
21
|
+
'SemesterId' => { 'type' => %w(integer null) },
|
|
22
|
+
'StartDate' => { 'type' => %w(string null) },
|
|
23
|
+
'EndDate' => { 'type' => %w(string null) },
|
|
24
|
+
'LocaleId' => { 'type' => %w(integer null) },
|
|
25
|
+
'ForceLocale' => { 'type' => 'boolean' },
|
|
26
|
+
'ShowAddressBook' => { 'type' => 'boolean' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
JSON::Validator.validate!(schema, course_data, validate_schema: true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Creates the course based upon a merged result of the argument course_data
|
|
34
|
+
# and a preformatted payload. This is then passed as a new payload in the
|
|
35
|
+
# +_post+ method in order to create the defined course.
|
|
36
|
+
# Required: "Name", "Code"
|
|
37
|
+
# Creates the course offering
|
|
38
|
+
def create_course_data(course_data)
|
|
39
|
+
# ForceLocale- course override the user’s locale preference
|
|
40
|
+
# Path- root path to use for this course offering’s course content
|
|
41
|
+
# if your back-end service has path enforcement set on for
|
|
42
|
+
# new org units, leave this property as an empty string
|
|
43
|
+
# Define a valid, empty payload and merge! with the user_data. Print it.
|
|
44
|
+
# can be an issue if more than one course template associated with
|
|
45
|
+
# a course and the last course template parent to a course cannot be deleted
|
|
46
|
+
payload = { 'Name' => '', # String
|
|
47
|
+
'Code' => 'off_SEMESTERCODE_STARNUM', # String
|
|
48
|
+
'Path' => '', # String
|
|
49
|
+
'CourseTemplateId' => 99_989, # number: D2L_ID
|
|
50
|
+
'SemesterId' => nil, # number: D2L_ID | nil
|
|
51
|
+
'StartDate' => nil, # String: UTCDateTime | nil
|
|
52
|
+
'EndDate' => nil, # String: UTCDateTime | nil
|
|
53
|
+
'LocaleId' => nil, # number: D2L_ID | nil
|
|
54
|
+
'ForceLocale' => false, # bool
|
|
55
|
+
'ShowAddressBook' => false # bool
|
|
56
|
+
}.merge!(course_data)
|
|
57
|
+
check_course_data_validity(payload)
|
|
58
|
+
# ap payload
|
|
59
|
+
# requires: CreateCourseOffering JSON block
|
|
60
|
+
path = "/d2l/api/lp/#{$lp_ver}/courses/"
|
|
61
|
+
_post(path, payload)
|
|
62
|
+
puts '[+] Course creation completed successfully'.green
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# In order to retrieve an entire department's class list, this method uses a
|
|
66
|
+
# predefined org_unit identifier. This identifier is then appended to a path
|
|
67
|
+
# and all classes withiin the department are returned as JSON objects in an arr.
|
|
68
|
+
#
|
|
69
|
+
# returns: JSON array of classes.
|
|
70
|
+
def get_org_department_classes(org_unit_id)
|
|
71
|
+
path = "/d2l/api/lp/#{$lp_ver}/orgstructure/#{org_unit_id}"
|
|
72
|
+
_get(path)
|
|
73
|
+
# returns: JSON array of classes.
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Performs a get request to retrieve a particular course using the org_unit_id
|
|
77
|
+
# of this particular course. If the course does not exist, as specified by the
|
|
78
|
+
# org_unit_id, the response is typically a 404 error.
|
|
79
|
+
#
|
|
80
|
+
# returns: JSON object of the course
|
|
81
|
+
def get_course_by_id(org_unit_id)
|
|
82
|
+
path = "/d2l/api/lp/#{$lp_ver}/courses/#{org_unit_id}"
|
|
83
|
+
_get(path)
|
|
84
|
+
# returns: JSON object of the course
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def get_all_courses
|
|
88
|
+
path = "/d2l/api/lp/#{$lp_ver}/orgstructure/6606/descendants/?ouTypeId=3"
|
|
89
|
+
_get(path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# much slower means of getting courses if less than 100 courses
|
|
93
|
+
def get_courses_by_code(org_unit_code)
|
|
94
|
+
all_courses = get_all_courses
|
|
95
|
+
courses = []
|
|
96
|
+
all_courses.each do |course|
|
|
97
|
+
courses.push(course) if course["Code"].downcase.include? "#{org_unit_code}".downcase
|
|
98
|
+
end
|
|
99
|
+
courses
|
|
100
|
+
end
|
|
101
|
+
# Retrieves all courses that have a particular string (org_unit_name) within
|
|
102
|
+
# their names. This is done by first defining that none are found yet and then
|
|
103
|
+
# searching through all course for ones that do have a particular string within
|
|
104
|
+
# their name, the matches are pushed into the previously empty array of matches.
|
|
105
|
+
# This array is subsequently returned; if none were found, a message is returned
|
|
106
|
+
#
|
|
107
|
+
# returns: JSON array of matching course data objects
|
|
108
|
+
def get_courses_by_name(org_unit_name)
|
|
109
|
+
get_courses_by_property_by_string('Name', org_unit_name)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Retrieves all matching courses that are found using a property and a search
|
|
113
|
+
# string. First, it is considered that the class is not found. Then, all courses
|
|
114
|
+
# are retrieved and stored as a JSON array in the varaible +results+. After this
|
|
115
|
+
# each of the +results+ is iterated, downcased, and checked for their matching
|
|
116
|
+
# of the particular search string. If there is a match, they are pushed to
|
|
117
|
+
# an array called +courses_results+. This is returned at the end of this op.
|
|
118
|
+
#
|
|
119
|
+
# returns: array of JSON course objects (that match the search string/property)
|
|
120
|
+
def get_courses_by_property_by_string(property, search_string)
|
|
121
|
+
puts "[+] Searching for courses using search string: #{search_string}".yellow +
|
|
122
|
+
+ " -- And property: #{property}"
|
|
123
|
+
courses_results = []
|
|
124
|
+
results = get_all_courses
|
|
125
|
+
results.each do |x|
|
|
126
|
+
if x[property].downcase.include? search_string.downcase
|
|
127
|
+
courses_results.push(x)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
courses_results
|
|
131
|
+
# returns array of all matching courses in JSON format.
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Retrieves all courses that have the specified prop match a regular expression.
|
|
135
|
+
# This is done by iterating through all courses and returning an array of all
|
|
136
|
+
# that match a regular expression.
|
|
137
|
+
#
|
|
138
|
+
# returns: array of JSON course objects (with property that matches regex)
|
|
139
|
+
def get_courses_by_property_by_regex(property, regex)
|
|
140
|
+
puts "[+] Searching for courses using regex: #{regex}".yellow +
|
|
141
|
+
+ " -- And property: #{property}"
|
|
142
|
+
courses_results = []
|
|
143
|
+
results = get_all_courses
|
|
144
|
+
results.each do |x|
|
|
145
|
+
courses_results.push(x) if (x[property] =~ regex) != nil
|
|
146
|
+
end
|
|
147
|
+
courses_results
|
|
148
|
+
# returns array of all matching courses in JSON format.
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Checks whether the updated course data conforms to the valence api for the
|
|
152
|
+
# update data JSON object. If it does conform, then nothing happens and it
|
|
153
|
+
# simply returns true. If it does not conform, then the JSON validator raises
|
|
154
|
+
# an exception.
|
|
155
|
+
def check_updated_course_data_validity(course_data)
|
|
156
|
+
schema = {
|
|
157
|
+
'type' => 'object',
|
|
158
|
+
'required' => %w(Name Code StartDate EndDate IsActive),
|
|
159
|
+
'properties' => {
|
|
160
|
+
'Name' => { 'type' => 'string' },
|
|
161
|
+
'Code' => { 'type' => 'string' },
|
|
162
|
+
'StartDate' => { 'type' => ['string', "null"] },
|
|
163
|
+
'EndDate' => { 'type' => ['string', "null"] },
|
|
164
|
+
'IsActive' => { 'type' => "boolean" },
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
JSON::Validator.validate!(schema, course_data, validate_schema: true)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Update the course based upon the first argument. This course object is first
|
|
171
|
+
# referenced via the first argument and its data formatted via merging it with
|
|
172
|
+
# a predefined payload. Then, a PUT http method is executed using the new
|
|
173
|
+
# payload.
|
|
174
|
+
# Utilize the second argument and perform a PUT action to replace the old data
|
|
175
|
+
def update_course_data(course_id, new_data)
|
|
176
|
+
# Define a valid, empty payload and merge! with the new data.
|
|
177
|
+
payload = { 'Name' => '', # String
|
|
178
|
+
'Code' => 'off_SEMESTERCODE_STARNUM', # String
|
|
179
|
+
'StartDate' => nil, # String: UTCDateTime | nil
|
|
180
|
+
'EndDate' => nil, # String: UTCDateTime | nil
|
|
181
|
+
'IsActive' => false # bool
|
|
182
|
+
}.merge!(new_data)
|
|
183
|
+
check_updated_course_data_validity(payload)
|
|
184
|
+
# ap payload
|
|
185
|
+
# Define a path referencing the courses path
|
|
186
|
+
path = "/d2l/api/lp/#{$lp_ver}/courses/" + course_id.to_s
|
|
187
|
+
_put(path, payload)
|
|
188
|
+
# requires: CourseOfferingInfo JSON block
|
|
189
|
+
puts '[+] Course update completed successfully'.green
|
|
190
|
+
# Define a path referencing the course data using the course_id
|
|
191
|
+
# Perform the put action that replaces the old data
|
|
192
|
+
# Provide feedback that the update was successful
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def is_course_component(key)
|
|
196
|
+
valid_components = %w(AttendanceRegisters Glossary News Checklists
|
|
197
|
+
Grades QuestionLibrary Competencies GradesSettings
|
|
198
|
+
Quizzes Content Groups ReleaseConditions CourseFiles
|
|
199
|
+
Homepages Rubrics Discussions IntelligentAgents
|
|
200
|
+
Schedule DisplaySettings Links SelfAssessments
|
|
201
|
+
Dropbox LtiLink Surveys Faq LtiTP ToolNames Forms
|
|
202
|
+
Navbars Widgets)
|
|
203
|
+
valid_components.include?(key)
|
|
204
|
+
# returns whether the key is actually a course component
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def check_create_copy_job_request_validity(create_copy_job_request)
|
|
208
|
+
schema = {
|
|
209
|
+
'type' => 'object',
|
|
210
|
+
'required' => %w(SourceOrgUnitId Components CallbackUrl),
|
|
211
|
+
'properties' => {
|
|
212
|
+
'SourceOrgUnitId' => { 'type' => 'integer' },
|
|
213
|
+
'Components' => {
|
|
214
|
+
'type' => ['array', "null"],
|
|
215
|
+
'items' =>
|
|
216
|
+
{
|
|
217
|
+
'type' => "string"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
'CallbackUrl' => { 'type' => ['string', 'null'] }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
JSON::Validator.validate!(schema, create_copy_job_request, validate_schema: true)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def create_new_copy_job_request(org_unit_id, create_copy_job_request)
|
|
228
|
+
payload =
|
|
229
|
+
{
|
|
230
|
+
'SourceOrgUnitId' => 0, # int
|
|
231
|
+
'Components' => nil, # [Str,...] || nil
|
|
232
|
+
'CallbackUrl' => nil # str | nil
|
|
233
|
+
}.merge!(create_copy_job_request)
|
|
234
|
+
# Check that the payload conforms to the JSON Schema of CreateCopyJobRequest
|
|
235
|
+
check_create_copy_job_request_validity(payload)
|
|
236
|
+
# Check each one of the components to see if they are valid Component types
|
|
237
|
+
payload["Components"].each do |component|
|
|
238
|
+
# If one of the components is not valid, cancel the CopyJobRequest operation
|
|
239
|
+
if(!is_course_component(key))
|
|
240
|
+
puts "'#{component}' specified is not a valid Copy Job Request component"
|
|
241
|
+
puts "Please retry with a valid course component such as 'Dropbox' or 'Grades'"
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
path = "/d2l/api/le/#{$le_ver}/import/#{org_unit_id}/copy/"
|
|
246
|
+
_post(path, payload)
|
|
247
|
+
# Returns CreateCopyJobResponse JSON block
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def get_copy_job_request_status(org_unit_id, job_token)
|
|
251
|
+
path = "/d2l/api/le/#{le_ver}/import/#{org_unit_id}/copy/#{job_token}"
|
|
252
|
+
_get(path)
|
|
253
|
+
# returns GetCopyJobResponse JSON block
|
|
254
|
+
# GetImportJobResponse:
|
|
255
|
+
# {"JobToken" => <string:COPYJOBSTATUS_T>,
|
|
256
|
+
# "TargetOrgUnitID" => <number:D2LID>,
|
|
257
|
+
# "Status" => <string:IMPORTJOBTSTATUS_T>}
|
|
258
|
+
# States of getImport: UPLOADING, PROCESSING, PROCESSED, IMPORTING,
|
|
259
|
+
# IMPORTFAILED, COMPLETED
|
|
260
|
+
end
|
|
261
|
+
#########
|
|
262
|
+
=begin
|
|
263
|
+
def create_course_import_request(org_unit_id, callback_url = '')
|
|
264
|
+
path = "/d2l/le/#{le_ver}/import/#{org_unit_id}/imports/"
|
|
265
|
+
path += "?callbackUrl=#{callback_url}" if callback_url != ''
|
|
266
|
+
#_post(path, payload)
|
|
267
|
+
#_upload(path, json, file, 'POST', 'file', filename)
|
|
268
|
+
|
|
269
|
+
end
|
|
270
|
+
=end
|
|
271
|
+
def get_course_import_job_request_status(org_unit_id, job_token)
|
|
272
|
+
path = "/d2l/api/le/#{le_ver}/import/#{org_unit_id}/imports/#{job_token}"
|
|
273
|
+
_get(path)
|
|
274
|
+
# returns GetImportJobResponse JSON block
|
|
275
|
+
# example:
|
|
276
|
+
# {"JobToken" => <string:COPYJOBSTATUS_T>}
|
|
277
|
+
# States: PENDING, PROCESSING, COMPLETE, FAILED, CANCELLED
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def get_course_import_job_request_logs(org_unit_id, job_token, bookmark = '')
|
|
281
|
+
path = "/d2l/api/le/#{le_ver}/import/#{org_unit_id}/imports/#{job_token}/logs"
|
|
282
|
+
path += "?bookmark=#{bookmark}" if bookmark != ''
|
|
283
|
+
_get(path)
|
|
284
|
+
# returns PAGED RESULT of ImportCourseLog JSON blocks following bookmark param
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Deletes a course based, referencing it via its org_unit_id
|
|
288
|
+
# This reference is created through a formatted path appended with the id.
|
|
289
|
+
# Then, a delete http method is executed using this path, deleting the course.
|
|
290
|
+
def delete_course_by_id(org_unit_id)
|
|
291
|
+
path = "/d2l/api/lp/#{$lp_ver}/courses/#{org_unit_id}" # setup user path
|
|
292
|
+
#ap path
|
|
293
|
+
_delete(path)
|
|
294
|
+
puts '[+] Course data deleted successfully'.green
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# retrieve the list of parent org unit type constraints for course offerings
|
|
298
|
+
def get_parent_outypes_courses_schema_constraints
|
|
299
|
+
path = "/d2l/api/lp/#{$lp_ver}/courses/schema"
|
|
300
|
+
_get(path)
|
|
301
|
+
# returns a JSON array of SchemaElement blocks
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def get_course_image(org_unit_id, width = 0, height = 0)
|
|
305
|
+
path = "/d2l/api/lp/#{lp_ver}/courses/#{org_unit_id}/image"
|
|
306
|
+
if width > 0 && height > 0
|
|
307
|
+
path += "?width=#{width}"
|
|
308
|
+
path += "&height=#{height}"
|
|
309
|
+
end
|
|
310
|
+
_get(path)
|
|
311
|
+
end
|