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.
@@ -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"
@@ -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