kgrift 1.3.108
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/KGrift/Gemfile +22 -0
- data/KGrift/README.md +66 -0
- data/KGrift/bin/kgrift +11 -0
- data/KGrift/grifter.yml +224 -0
- data/KGrift/internal_test_graphs/basic_test_graph_definition.yml +2915 -0
- data/KGrift/internal_test_graphs/unicode_test_graph_definition.yml +3070 -0
- data/KGrift/knewton_grifts/analytics_grifts.rb +103 -0
- data/KGrift/knewton_grifts/async_helper_grifts.rb +63 -0
- data/KGrift/knewton_grifts/authenticator_grifts.rb +46 -0
- data/KGrift/knewton_grifts/basic_grifts.rb +29 -0
- data/KGrift/knewton_grifts/batch_grifts.rb +14 -0
- data/KGrift/knewton_grifts/content_collection_grifts.rb +204 -0
- data/KGrift/knewton_grifts/content_collection_v1_grifts.rb +521 -0
- data/KGrift/knewton_grifts/content_eid_grifts.rb +41 -0
- data/KGrift/knewton_grifts/copy_grifts.rb +151 -0
- data/KGrift/knewton_grifts/deprecated_graph_and_taxonomy_grifts.rb +353 -0
- data/KGrift/knewton_grifts/goal_grifts.rb +203 -0
- data/KGrift/knewton_grifts/graph_and_taxonomy_grifts.rb +136 -0
- data/KGrift/knewton_grifts/graph_create_grifts.rb +34 -0
- data/KGrift/knewton_grifts/graph_query_grifts.rb +448 -0
- data/KGrift/knewton_grifts/graph_tools_grifts.rb +151 -0
- data/KGrift/knewton_grifts/graph_validation_grifts.rb +447 -0
- data/KGrift/knewton_grifts/helper_grifts.rb +92 -0
- data/KGrift/knewton_grifts/jmeter_data_grifts.rb +56 -0
- data/KGrift/knewton_grifts/learning_instance_grifts.rb +46 -0
- data/KGrift/knewton_grifts/looper_grifts.rb +34 -0
- data/KGrift/knewton_grifts/moxy_grifts.rb +64 -0
- data/KGrift/knewton_grifts/oauth_grifts.rb +182 -0
- data/KGrift/knewton_grifts/partner_grifts.rb +70 -0
- data/KGrift/knewton_grifts/partner_support_grifts.rb +85 -0
- data/KGrift/knewton_grifts/recommendation_setup_grifts.rb +215 -0
- data/KGrift/knewton_grifts/registration_grifts.rb +159 -0
- data/KGrift/knewton_grifts/registration_info_grifts.rb +23 -0
- data/KGrift/knewton_grifts/report_grifts.rb +122 -0
- data/KGrift/knewton_grifts/shell_command_grifts.rb +21 -0
- data/KGrift/knewton_grifts/student_flow_grifts.rb +560 -0
- data/KGrift/knewton_grifts/tag_grifts.rb +41 -0
- data/KGrift/knewton_grifts/test_data_grifts.rb +328 -0
- data/KGrift/knewton_grifts/test_user_grifts.rb +264 -0
- data/KGrift/lib/dtrace.rb +20 -0
- data/KGrift/lib/kgrift.rb +7 -0
- data/KGrift/test_data_generators/basic_book_and_taxonomies.rb +35 -0
- data/KGrift/test_data_generators/lo_test_graph.rb +34 -0
- data/KGrift/test_data_generators/partner_owned_book_and_taxonomies.rb +28 -0
- data/KGrift/test_data_generators/partner_owned_book_and_taxonomies_unicode.rb +28 -0
- data/KGrift/test_data_generators/sandcastle_book_and_taxonomies.rb +13 -0
- data/KGrift/test_data_generators/sandcastle_book_and_taxonomies.yml +3709 -0
- data/KGrift/test_data_generators/sandcastle_graph.rb +8 -0
- data/KGrift/test_data_generators/sandcastle_graph_definition.json +4483 -0
- data/KGrift/test_data_generators/sandcastle_graph_full.rb +7 -0
- data/KGrift/test_data_generators/sandcastle_taxonomies.yml +378 -0
- data/KGrift/test_data_generators/sandcastle_with_taxons.rb +56 -0
- data/KGrift/test_data_generators/sandcastle_with_taxons.yml +3994 -0
- data/KGrift/test_data_generators/test_users_and_partners.rb +76 -0
- data/kgrift.gemspec +43 -0
- metadata +144 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
"""
|
2
|
+
GraphTools is the rest api begind the Bramble react application
|
3
|
+
|
4
|
+
These grifts are used to power tests made directly against the graphtools
|
5
|
+
api.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# the way we authenticate for graphtools api is different from other services
|
9
|
+
# other services use the knewton oauth2 provider.
|
10
|
+
# graphtools uses google oauth2 provider in such a way that restricts the app/api to only those with @knewton.com emails
|
11
|
+
#
|
12
|
+
# The complexity of getting a google token is completely hidden in the graphtools_api_token test data generator
|
13
|
+
def graphtools_authenticate
|
14
|
+
# these google tokens last something like 12ish hours...
|
15
|
+
# so we use the session_only option to get a single fresh one on each test run
|
16
|
+
token = get_test_data(:graphtools_api_token, session_only: true)['token']
|
17
|
+
graphtools.headers['Authorization'] = token
|
18
|
+
# return true to mean success
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Methods that adds additional headers needed by the graph editor endpoints
|
23
|
+
def graph_editor_get partner_id, inventory_id, url
|
24
|
+
graphtools.get url, additional_headers: {
|
25
|
+
'Partner-Id' => partner_id,
|
26
|
+
'Inventory-Id' => inventory_id,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def graph_editor_post partner_id, inventory_id, url, body
|
31
|
+
graphtools.post url, body, additional_headers: {
|
32
|
+
'Partner-Id' => partner_id,
|
33
|
+
'Inventory-Id' => inventory_id,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def graph_editor_delete partner_id, inventory_id, url, body = {}
|
38
|
+
graphtools.do_request :delete, url, body, additional_headers: {
|
39
|
+
'Partner-Id' => partner_id,
|
40
|
+
'Inventory-Id' => inventory_id,
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Endpoints for graph editor
|
45
|
+
# Pulling all modules from CoCo for a specific inventory
|
46
|
+
def pull_modules_from_coco partner_id, inventory_id
|
47
|
+
graph_editor_post partner_id, inventory_id, "/graph", {}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Clearing all the concepts and modules for a specific inventory
|
51
|
+
def clear_graph_tools partner_id, inventory_id
|
52
|
+
graph_editor_post partner_id, inventory_id, "/graph/clear", {}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Getting the difference between modules in CoCo and GraphTools
|
56
|
+
def graph_diff partner_id, inventory_id
|
57
|
+
graph_editor_get partner_id, inventory_id, "/graph/delta"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Getting number of modules that are different in CoCo and GraphTools
|
61
|
+
def graph_diff_summary partner_id, inventory_id
|
62
|
+
graph_editor_get partner_id, inventory_id, "/graph/delta-summary"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create concept for an inventory
|
66
|
+
def graphtools_create_concept partner_id, inventory_id, concept={}
|
67
|
+
graph_editor_post partner_id, inventory_id, "/concepts/concept", concept
|
68
|
+
end
|
69
|
+
|
70
|
+
# Update concept for an inventory
|
71
|
+
def graphtools_update_concept partner_id, inventory_id, concept={}
|
72
|
+
graph_editor_post partner_id, inventory_id, "/concepts/concept/update", concept
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get a specific concept for an inventory
|
76
|
+
def graphtools_get_concept partner_id, inventory_id, concept_id
|
77
|
+
graph_editor_get partner_id, inventory_id, "/concepts/concept?#{URI.encode_www_form concept_id}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Delete a specific concept for an inventory
|
81
|
+
def graphtools_delete_concept partner_id, inventory_id, concept_id
|
82
|
+
graph_editor_delete partner_id, inventory_id, "/concepts/concept/#{URI.encode concept_id}"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get a specific module from an inventory
|
86
|
+
def graphtools_get_module partner_id, inventory_id, module_id
|
87
|
+
graph_editor_get partner_id, inventory_id, "/modules/module?#{URI.encode_www_form module_id}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Getting modules from an inventory
|
91
|
+
def graphtools_get_modules partner_id, inventory_id, query_params={}
|
92
|
+
graph_editor_get partner_id, inventory_id, "/modules?#{URI.encode_www_form query_params}"
|
93
|
+
end
|
94
|
+
# Getting modules from an inventory
|
95
|
+
def graphtools_get_concepts partner_id, inventory_id, query_params={}
|
96
|
+
graph_editor_get partner_id, inventory_id, "/concepts?#{URI.encode_www_form query_params}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create a concept-concept edge and return the updated concept
|
100
|
+
def graphtools_create_cc_edge partner_id, inventory_id, edge
|
101
|
+
graph_editor_post partner_id, inventory_id, "/concepts/edge/concept", edge
|
102
|
+
end
|
103
|
+
# Delete a concept-concept edge and return the updated concept
|
104
|
+
def graphtools_delete_cc_edge partner_id, inventory_id, edge
|
105
|
+
graph_editor_delete partner_id, inventory_id, "/concepts/edge/concept", edge
|
106
|
+
end
|
107
|
+
|
108
|
+
# Create a concept-module edge and return the updated concept
|
109
|
+
def graphtools_create_cm_edge partner_id, inventory_id, edge
|
110
|
+
graph_editor_post partner_id, inventory_id, "/concepts/edge/module", edge
|
111
|
+
end
|
112
|
+
# Delete a concept-module edge and return the updated concept
|
113
|
+
def graphtools_delete_cm_edge partner_id, inventory_id, edge
|
114
|
+
graph_editor_delete partner_id, inventory_id, "/concepts/edge/module", edge
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create a concept-module edge and return the updated module
|
118
|
+
def graphtools_create_mc_edge partner_id, inventory_id, edge
|
119
|
+
graph_editor_post partner_id, inventory_id, "/modules/edge/concept", edge
|
120
|
+
end
|
121
|
+
# Delete a concept-module edge and return the updated module
|
122
|
+
def graphtools_delete_mc_edge partner_id, inventory_id, edge
|
123
|
+
graph_editor_delete partner_id, inventory_id, "/modules/edge/concept", edge
|
124
|
+
end
|
125
|
+
|
126
|
+
# this is a unique function in that it writes a spreadsheet file
|
127
|
+
# the return is the path to the spreadsheet
|
128
|
+
def graphtools_get_graph_spreadsheet partner_id, inventory_id
|
129
|
+
# we need to customize the headers for this to work...
|
130
|
+
# so this request is being constructed in a very custom way...
|
131
|
+
url = "/graph/spreadsheet"
|
132
|
+
response = graphtools.get url, additional_headers: {
|
133
|
+
'Partner-Id' => partner_id,
|
134
|
+
'Inventory-Id' => inventory_id,
|
135
|
+
'accept' => '*/*',
|
136
|
+
}
|
137
|
+
# response is the binary of the spreadsheet. Just dump it into a file...
|
138
|
+
# fist make a path to a file
|
139
|
+
# path is designed to be a) findable by including inventory_id / partner_id
|
140
|
+
# b) random, so that no possibility of tests using wrong file occurs
|
141
|
+
tmp_folder = 'tmp'
|
142
|
+
suppress(Exception) { Dir.mkdir tmp_folder }
|
143
|
+
filename = "test_spreadsheet_#{partner_id}_#{inventory_id}_#{random_string(8)}.xlsx"
|
144
|
+
path = File.join tmp_folder, filename
|
145
|
+
# second write the response into the file
|
146
|
+
File.open(path, 'w') do |f|
|
147
|
+
f.write response
|
148
|
+
end
|
149
|
+
path
|
150
|
+
end
|
151
|
+
|
@@ -0,0 +1,447 @@
|
|
1
|
+
"""
|
2
|
+
This hunk of code is all about validating whether a known graph id 'works'.
|
3
|
+
It does this by sending student events against every module in the graph,
|
4
|
+
ensuring in each case a new recommentation is produced
|
5
|
+
"""
|
6
|
+
|
7
|
+
def send_all_events_against_graph_and_goal graph_id, learning_instance_id, goal_id, num_regs
|
8
|
+
#cache the graph
|
9
|
+
graph = get_graph_and_cache_it graph_id
|
10
|
+
|
11
|
+
#create a num_regs new students and activate on the same goal
|
12
|
+
students = []
|
13
|
+
num_regs.to_i.times do
|
14
|
+
#add new student and activate on goal
|
15
|
+
student = (add_new_student_to_learning_instance learning_instance_id)['registration_id']
|
16
|
+
activate_registration_on_goal learning_instance_id, student, goal_id
|
17
|
+
#get an initial recommendation
|
18
|
+
get_next_recommendation_in_flow student, goal_id, nil
|
19
|
+
#save the student registration id
|
20
|
+
students << student
|
21
|
+
end
|
22
|
+
|
23
|
+
#iterate over all concepts
|
24
|
+
all_concepts = find_all_concept_ids graph
|
25
|
+
Log.info "Found #{all_concepts.length} concepts in total"
|
26
|
+
all_concepts.each do |concept|
|
27
|
+
Log.info "CONCEPT #{all_concepts.index(concept)+1}/#{all_concepts.length}"
|
28
|
+
Log.info "Finding all associated modules for concept #{concept}"
|
29
|
+
#find all modules by type
|
30
|
+
all_modules = []
|
31
|
+
all_modules << find_associated_assessing_atoms(graph,concept).map{|m| {'id' => m,
|
32
|
+
'type' => 'atom',
|
33
|
+
'content' => 'assessment'}}
|
34
|
+
all_modules << find_associated_assessing_bndls(graph,concept).map{|m| {'id' => m,
|
35
|
+
'type' => 'bndl',
|
36
|
+
'content' => 'assessment'}}
|
37
|
+
all_modules << find_associated_instructional_atoms(graph,concept).map{|m| {'id' => m,
|
38
|
+
'type' => 'atom',
|
39
|
+
'content' => 'instructional'}}
|
40
|
+
all_modules << find_associated_instructional_bndls(graph,concept).map{|m| {'id' => m,
|
41
|
+
'type' => 'bndl',
|
42
|
+
'content' => 'instructional'}}
|
43
|
+
#iterate over all modules
|
44
|
+
#filter out duplicates and give preference to assessmen content (graded events)
|
45
|
+
mod_list = []
|
46
|
+
all_modules.flatten.each do |mod|
|
47
|
+
next if mod_list.include?(mod['id'])
|
48
|
+
student = students.rotate!.first
|
49
|
+
#send events by module category
|
50
|
+
if mod['type'] == 'bndl' or mod['content'] == 'instructional'
|
51
|
+
Log.info "Sending ungraded event for #{mod['content']} #{mod['type']} #{mod['id']}"
|
52
|
+
send_ungraded_event student, mod['id']
|
53
|
+
else
|
54
|
+
Log.info "Sending graded event for #{mod['content']} #{mod['type']} #{mod['id']}"
|
55
|
+
send_graded_event student, mod['id']
|
56
|
+
end
|
57
|
+
mod_list << mod['id']
|
58
|
+
puts ""
|
59
|
+
#slow down the rate to below 15r/s
|
60
|
+
sleep(2.0/10.0)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
#get a final recommendation for each registration
|
65
|
+
Log.info "Getting a final recommendation"
|
66
|
+
num_regs.to_i.times do |i|
|
67
|
+
get_next_recommendation_in_flow students[i], goal_id, nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def complete_goal_based_on_graph graph_id
|
72
|
+
#get graph
|
73
|
+
graph = get_graph_and_cache_it graph_id
|
74
|
+
puts "Loaded and cached graph #{graph_id}"
|
75
|
+
|
76
|
+
#get recommendable modules (based on graph)
|
77
|
+
rec_modules = find_all_recommendable_module_ids graph
|
78
|
+
puts "Number of recommendable modules: #{rec_modules.length}"
|
79
|
+
|
80
|
+
#define goal choosing prereq module
|
81
|
+
target_concept = find_prerequisite_concept graph
|
82
|
+
target_bndl = find_associated_assessing_bndl graph, target_concept
|
83
|
+
goal_obj = {
|
84
|
+
'target_modules' => [
|
85
|
+
{
|
86
|
+
'module_id' => target_bndl,
|
87
|
+
'target_date' => days_from_now(100),
|
88
|
+
'target_score' => 0.6,
|
89
|
+
}
|
90
|
+
],
|
91
|
+
'max_recommendation_size' => 3,
|
92
|
+
'start_date' => datetime,
|
93
|
+
'recommendable_modules' => rec_modules.map{|m| {'module_id' => m}},
|
94
|
+
}
|
95
|
+
learning_instance = create_learning_instance 'graph_id' => graph['id']
|
96
|
+
goal = create_goal learning_instance['id'], goal_obj
|
97
|
+
puts "Created learning instance #{learning_instance['id']}; using goal-id #{goal['id']}"
|
98
|
+
|
99
|
+
#add student to learning instance
|
100
|
+
student = add_new_student_to_learning_instance learning_instance['id']
|
101
|
+
activate_registration_on_goal learning_instance['id'], student['registration_id'], goal['id']
|
102
|
+
puts "Added student and activated registration"
|
103
|
+
|
104
|
+
#complete goal
|
105
|
+
completion_data = complete_goal student['registration_id'], goal['id']
|
106
|
+
|
107
|
+
#delete registration and learning instance
|
108
|
+
delete_registration student['registration_id']
|
109
|
+
delete_learning_instance learning_instance['id']
|
110
|
+
|
111
|
+
#return goal completion data
|
112
|
+
completion_data
|
113
|
+
end
|
114
|
+
|
115
|
+
def graph_passes_mega_validation? graph_id
|
116
|
+
graph_passes_mega_validation_multithreaded? graph_id, 1
|
117
|
+
end
|
118
|
+
|
119
|
+
def graph_passes_mega_validation_multithreaded? graph_id, num_threads
|
120
|
+
#get graph
|
121
|
+
graph = get_graph_and_cache_it graph_id
|
122
|
+
puts "Loaded and cached graph: #{graph_id}"
|
123
|
+
|
124
|
+
#get recommendable modules (based on graph)
|
125
|
+
all_concepts = find_all_concept_ids graph
|
126
|
+
puts "Number of concepts: #{all_concepts.length}"
|
127
|
+
|
128
|
+
#define goal (choose post-requisite concept as target)
|
129
|
+
target_concept = find_post_requisite_concept graph
|
130
|
+
target_bndl = find_associated_assessing_bndl graph, target_concept
|
131
|
+
if target_bndl==nil
|
132
|
+
puts "Validation FAILED: concept #{target_concept} has no associated assessment bundle."
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
rec_modules = find_all_recommendable_module_ids graph
|
136
|
+
goal_obj = {
|
137
|
+
'target_modules' => [
|
138
|
+
{
|
139
|
+
'module_id' => target_bndl,
|
140
|
+
'target_date' => days_from_now(100),
|
141
|
+
'target_score' => 0.99,
|
142
|
+
}
|
143
|
+
],
|
144
|
+
'max_recommendation_size' => 3,
|
145
|
+
'start_date' => datetime,
|
146
|
+
'recommendable_modules' => rec_modules.map{|m| {'module_id' => m}},
|
147
|
+
}
|
148
|
+
|
149
|
+
#Fork n threads
|
150
|
+
mutex = Mutex.new
|
151
|
+
abort_mutex = Mutex.new
|
152
|
+
abort_flag = 0
|
153
|
+
fail_flag = Array.new(num_threads.to_i, 0)
|
154
|
+
learning_id = Array.new(num_threads.to_i,nil)
|
155
|
+
reg_id = Array.new(num_threads.to_i,nil)
|
156
|
+
threads = (0..(num_threads.to_i-1)).map do |i|
|
157
|
+
Thread.new(i) do |i|
|
158
|
+
#create unique learning instance and activate registration
|
159
|
+
goal = nil
|
160
|
+
student = nil
|
161
|
+
mutex.synchronize do
|
162
|
+
learning_instance = create_learning_instance 'graph_id' => graph['id']
|
163
|
+
learning_id[i] = learning_instance['id']
|
164
|
+
goal = create_goal learning_instance['id'], goal_obj
|
165
|
+
puts "Created learning instance #{learning_instance['id']} for goal id #{goal['id']}"
|
166
|
+
student = add_new_student_to_learning_instance learning_instance['id']
|
167
|
+
reg_id[i] = student['registration_id']
|
168
|
+
activate_registration_on_goal learning_instance['id'], student['registration_id'], goal['id']
|
169
|
+
puts "Added student and activated registration"
|
170
|
+
end
|
171
|
+
|
172
|
+
#define per-thread module partitions
|
173
|
+
start_concept = (all_concepts.length*i.to_f/num_threads.to_i).floor
|
174
|
+
end_concept = (all_concepts.length*(i.to_f+1)/num_threads.to_i).floor-1
|
175
|
+
|
176
|
+
#send events and grab recommendations for all modules
|
177
|
+
puts "Sending events and receiving recommendations..."
|
178
|
+
concept_counter = start_concept
|
179
|
+
all_concepts[start_concept..end_concept].each do |concept|
|
180
|
+
concept_counter += 1
|
181
|
+
puts ""
|
182
|
+
|
183
|
+
#exit when abort flag set
|
184
|
+
break if abort_flag==1
|
185
|
+
|
186
|
+
#skip iteration if concept is target concept
|
187
|
+
if (concept == target_concept)
|
188
|
+
puts "CONCEPT #{concept_counter}: skipped target concept: #{concept}"
|
189
|
+
next
|
190
|
+
end
|
191
|
+
|
192
|
+
#get next recommendation; set fail and abort flags if event duplicated
|
193
|
+
begin
|
194
|
+
current_recommendation = get_next_recommendation_in_flow student['registration_id'], goal['id'], current_recommendation
|
195
|
+
puts "CONCEPT #{concept_counter}: received new recommendation: #{current_recommendation['recommendation_id']}"
|
196
|
+
puts current_recommendation
|
197
|
+
rescue ExpectationError => error
|
198
|
+
fail_flag[i] = 1
|
199
|
+
puts "CONCEPT #{concept_counter}: received a duplicate recommendation: #{current_recommendation['recommendation_id']}"
|
200
|
+
abort_mutex.synchronize do
|
201
|
+
abort_flag = 1
|
202
|
+
end
|
203
|
+
puts "Validationn FAILED: recommendation duplicated: concept #{concept}."
|
204
|
+
break
|
205
|
+
end
|
206
|
+
|
207
|
+
#find current concept's associate assessing bundle and send graded event
|
208
|
+
assessing_bndl = find_associated_assessing_bndl graph, concept
|
209
|
+
assessing_bndl = (find_associated_assessing_atom graph, concept) unless assessing_bndl!=nil
|
210
|
+
if assessing_bndl==nil
|
211
|
+
fail_flag[i] = 1
|
212
|
+
puts "Validation FAILED: concept #{target_concept} has no associated assessment bundle."
|
213
|
+
abort_mutex.synchronize do
|
214
|
+
abort_flag = 1
|
215
|
+
end
|
216
|
+
break
|
217
|
+
end
|
218
|
+
puts "CONCEPT #{concept_counter}: concept #{concept} assessed by bundle #{assessing_bndl}"
|
219
|
+
event_body = generate_graded_event_body assessing_bndl, graph
|
220
|
+
mutex.synchronize do
|
221
|
+
send_graded_event student['registration_id'], nil, event_body
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
#get final recommendation
|
226
|
+
unless abort_flag==1
|
227
|
+
begin
|
228
|
+
current_recommendation = get_next_recommendation_in_flow student['registration_id'], goal['id'], current_recommendation
|
229
|
+
puts "CONCEPT #{concept_counter}: received new recommendation: #{current_recommendation['recommendation_id']}"
|
230
|
+
puts current_recommendation
|
231
|
+
rescue ExpectationError => error
|
232
|
+
fail_flag[i] = 1
|
233
|
+
puts "CONCEPT #{concept_counter}: received a duplicate recommendation: #{current_recommendation['recommendation_id']}"
|
234
|
+
abort_mutex.synchronize do
|
235
|
+
abort_flag = 1
|
236
|
+
end
|
237
|
+
puts "Validationn FAILED: recommendation duplicated following event sent to concept #{all_concepts[end_concept]}."
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
threads.each {|t| t.join}
|
243
|
+
|
244
|
+
#cleanup and return true, or return false if fail flag raised (without cleanup)
|
245
|
+
if fail_flag.reduce(:+) == 0
|
246
|
+
#delete registration and learning instance
|
247
|
+
(0..(num_threads.to_i-1)).each do |j|
|
248
|
+
delete_registration reg_id[j]
|
249
|
+
delete_learning_instance learning_id[j]
|
250
|
+
end
|
251
|
+
return true
|
252
|
+
else
|
253
|
+
return false
|
254
|
+
end
|
255
|
+
end
|
256
|
+
alias :graph_mega_validation :graph_passes_mega_validation_multithreaded?
|
257
|
+
|
258
|
+
def graph_passes_taxon_mega_validation_multithreaded? graph_id, num_threads
|
259
|
+
#get graph
|
260
|
+
graph = get_graph_and_cache_it graph_id
|
261
|
+
puts "Loaded and cached graph: #{graph_id}"
|
262
|
+
|
263
|
+
#get recommendable modules (based on graph)
|
264
|
+
all_concepts = find_all_concept_ids graph
|
265
|
+
all_atoms = graph['nodes'].select{|n| n['type']=='module' and
|
266
|
+
n['subtype']=='atom'
|
267
|
+
}.map{|n| n['id']}
|
268
|
+
all_bndls = graph['nodes'].select{|n| n['type']=='module' and
|
269
|
+
n['subtype']=='bndl'
|
270
|
+
}.map{|n| n['id']}
|
271
|
+
puts "Number of concepts: #{all_concepts.size}"
|
272
|
+
puts "Number of modules: #{(all_atoms+all_bndls).size}"
|
273
|
+
|
274
|
+
#get all taxons
|
275
|
+
all_taxons = get_all_graph_taxonomies graph
|
276
|
+
puts "Number of taxons: #{all_taxons.size}"
|
277
|
+
|
278
|
+
#define goal (choose post-requisite concept as target)
|
279
|
+
target_concept = find_post_requisite_concept graph
|
280
|
+
target_bndl = find_associated_assessing_bndl graph, target_concept
|
281
|
+
if target_bndl==nil
|
282
|
+
puts "Validation FAILED: concept #{target_concept} has no associated assessment bundle."
|
283
|
+
return false
|
284
|
+
end
|
285
|
+
rec_modules = find_all_recommendable_module_ids graph
|
286
|
+
goal_obj = {
|
287
|
+
'target_modules' => [
|
288
|
+
{
|
289
|
+
'module_id' => target_bndl,
|
290
|
+
'target_date' => days_from_now(100),
|
291
|
+
'target_score' => 0.99,
|
292
|
+
}
|
293
|
+
],
|
294
|
+
'max_recommendation_size' => 3,
|
295
|
+
'start_date' => datetime,
|
296
|
+
'recommendable_modules' => rec_modules.map{|m| {'module_id' => m}},
|
297
|
+
}
|
298
|
+
|
299
|
+
#get list of all assessment modules (bundles and atoms)
|
300
|
+
puts "Retrieving all assessment modules..."
|
301
|
+
assessment_modules = find_all_assessment_modules graph
|
302
|
+
puts "Number of assessment modules: #{assessment_modules.size}"
|
303
|
+
|
304
|
+
#Fork n threads
|
305
|
+
mutex = Mutex.new
|
306
|
+
abort_mutex = Mutex.new
|
307
|
+
abort_flag = 0
|
308
|
+
fail_flag = Array.new(num_threads.to_i, 0)
|
309
|
+
learning_id = Array.new(num_threads.to_i,nil)
|
310
|
+
reg_id = Array.new(num_threads.to_i,nil)
|
311
|
+
threads = (0..(num_threads.to_i-1)).map do |i|
|
312
|
+
Thread.new(i) do |i|
|
313
|
+
#create unique learning instance and activate registration
|
314
|
+
goal = nil
|
315
|
+
student = nil
|
316
|
+
mutex.synchronize do
|
317
|
+
learning_instance = create_learning_instance 'graph_id' => graph['id']
|
318
|
+
learning_id[i] = learning_instance['id']
|
319
|
+
goal = create_goal learning_instance['id'], goal_obj
|
320
|
+
puts "Created learning instance #{learning_instance['id']} for goal id #{goal['id']}"
|
321
|
+
student = add_new_student_to_learning_instance learning_instance['id']
|
322
|
+
reg_id[i] = student['registration_id']
|
323
|
+
activate_registration_on_goal learning_instance['id'], student['registration_id'], goal['id']
|
324
|
+
puts "Added student and activated registration"
|
325
|
+
end
|
326
|
+
|
327
|
+
#define per-thread module partitions
|
328
|
+
start_taxon = (all_taxons.length*i.to_f/num_threads.to_i).floor
|
329
|
+
end_taxon = (all_taxons.length*(i.to_f+1)/num_threads.to_i).floor-1
|
330
|
+
|
331
|
+
#send events and grab recommendations for all modules
|
332
|
+
puts "Sending events and receiving recommendations..."
|
333
|
+
taxon_counter = start_taxon
|
334
|
+
all_taxons[start_taxon..end_taxon].each do |taxon|
|
335
|
+
taxon_counter += 1
|
336
|
+
puts ""
|
337
|
+
|
338
|
+
#exit when abort flag set
|
339
|
+
if abort_flag==1
|
340
|
+
break
|
341
|
+
end
|
342
|
+
|
343
|
+
#get next recommendation; set fail and abort flags if event duplicated
|
344
|
+
begin
|
345
|
+
current_recommendation = get_next_recommendation_in_flow student['registration_id'], goal['id'], current_recommendation
|
346
|
+
puts "TAXON #{taxon_counter}: received new recommendation: #{current_recommendation['recommendation_id']}"
|
347
|
+
puts current_recommendation
|
348
|
+
rescue ExpectationError => error
|
349
|
+
fail_flag[i] = 1
|
350
|
+
puts "TAXON #{taxon_counter}: received a duplicate recommendation: #{current_recommendation['recommendation_id']}"
|
351
|
+
abort_mutex.synchronize do
|
352
|
+
abort_flag = 1
|
353
|
+
end
|
354
|
+
puts "Validationn FAILED: recommendation duplicated: taxon #{taxon}."
|
355
|
+
break
|
356
|
+
end
|
357
|
+
|
358
|
+
#find random module in current taxon
|
359
|
+
assessing_module = (get_modules_by_taxonomy graph, taxon).select{|module_id| assessment_modules.include?(module_id)}.sample
|
360
|
+
if assessing_module==nil
|
361
|
+
fail_flag[i] = 1
|
362
|
+
puts "Validation FAILED: taxon #{target_taxon} has no associated assessment atoms or bundles."
|
363
|
+
abort_mutex.synchronize do
|
364
|
+
abort_flag = 1
|
365
|
+
end
|
366
|
+
break
|
367
|
+
end
|
368
|
+
puts "TAXON #{taxon_counter}: taxon #{taxon} assessed by bundle #{assessing_module}"
|
369
|
+
event_body = generate_graded_event_body assessing_module, graph
|
370
|
+
mutex.synchronize do
|
371
|
+
send_graded_event student['registration_id'], nil, event_body
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
#get final recommendation
|
376
|
+
unless abort_flag==1
|
377
|
+
begin
|
378
|
+
current_recommendation = get_next_recommendation_in_flow student['registration_id'], goal['id'], current_recommendation
|
379
|
+
puts "TAXON #{taxon_counter}: received new recommendation: #{current_recommendation['recommendation_id']}"
|
380
|
+
puts current_recommendation
|
381
|
+
rescue ExpectationError => error
|
382
|
+
fail_flag[i] = 1
|
383
|
+
puts "TAXON #{taxon_counter}: received a duplicate recommendation: #{current_recommendation['recommendation_id']}"
|
384
|
+
abort_mutex.synchronize do
|
385
|
+
abort_flag = 1
|
386
|
+
end
|
387
|
+
puts "Validationn FAILED: recommendation duplicated following event sent to module #{assessing_module} in taxon #{all_taxons[end_taxon]}."
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
threads.each {|t| t.join}
|
393
|
+
|
394
|
+
#cleanup and return true, or return false if fail flag raised (without cleanup)
|
395
|
+
if fail_flag.reduce(:+) == 0
|
396
|
+
#delete registration and learning instance
|
397
|
+
(0..(num_threads.to_i-1)).each do |j|
|
398
|
+
delete_registration reg_id[j]
|
399
|
+
delete_learning_instance learning_id[j]
|
400
|
+
end
|
401
|
+
return true
|
402
|
+
else
|
403
|
+
return false
|
404
|
+
end
|
405
|
+
end
|
406
|
+
alias :graph_taxon_mega_validation :graph_passes_taxon_mega_validation_multithreaded?
|
407
|
+
|
408
|
+
|
409
|
+
#Helper function: generates custom event body for graded event; if bundle, chooses one atom at random
|
410
|
+
def generate_graded_event_body module_id, graph, options={}
|
411
|
+
node_type = get_node_type graph, module_id
|
412
|
+
atom_ids = case node_type
|
413
|
+
when 'atom'
|
414
|
+
[module_id]
|
415
|
+
when 'bndl'
|
416
|
+
[module_id] + graph['edges'].select{|e| e['type'] == 'contains' and e['start'] == module_id}.map{|e| e['end']}
|
417
|
+
end
|
418
|
+
pick_module = atom_ids.sample
|
419
|
+
puts "Sending event for module: #{pick_module}"
|
420
|
+
|
421
|
+
event_body = generate_event_body(:graded, options).merge({'module_id' => pick_module})
|
422
|
+
end
|
423
|
+
|
424
|
+
#traverses graph searching for concepts without assessment modules
|
425
|
+
def find_concepts_without_assessment_modules graph_id
|
426
|
+
graph = get_graph_and_cache_it graph_id
|
427
|
+
all_concepts = find_all_concept_ids graph
|
428
|
+
empty_concepts = []
|
429
|
+
all_concepts.each do |concept|
|
430
|
+
bndls=[]
|
431
|
+
atoms=[]
|
432
|
+
bndls = find_associated_assessing_bndl graph, concept
|
433
|
+
atoms = find_associated_assessing_atom graph, concept
|
434
|
+
if bndls==nil and atoms==nil
|
435
|
+
empty_concepts << concept
|
436
|
+
puts concept
|
437
|
+
end
|
438
|
+
end
|
439
|
+
puts ""
|
440
|
+
if empty_concepts.empty?
|
441
|
+
puts "All concepts have at least one assessment module"
|
442
|
+
else
|
443
|
+
puts "The following concepts do not have assessment modules:"
|
444
|
+
end
|
445
|
+
empty_concepts
|
446
|
+
end
|
447
|
+
|