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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/KGrift/Gemfile +22 -0
  3. data/KGrift/README.md +66 -0
  4. data/KGrift/bin/kgrift +11 -0
  5. data/KGrift/grifter.yml +224 -0
  6. data/KGrift/internal_test_graphs/basic_test_graph_definition.yml +2915 -0
  7. data/KGrift/internal_test_graphs/unicode_test_graph_definition.yml +3070 -0
  8. data/KGrift/knewton_grifts/analytics_grifts.rb +103 -0
  9. data/KGrift/knewton_grifts/async_helper_grifts.rb +63 -0
  10. data/KGrift/knewton_grifts/authenticator_grifts.rb +46 -0
  11. data/KGrift/knewton_grifts/basic_grifts.rb +29 -0
  12. data/KGrift/knewton_grifts/batch_grifts.rb +14 -0
  13. data/KGrift/knewton_grifts/content_collection_grifts.rb +204 -0
  14. data/KGrift/knewton_grifts/content_collection_v1_grifts.rb +521 -0
  15. data/KGrift/knewton_grifts/content_eid_grifts.rb +41 -0
  16. data/KGrift/knewton_grifts/copy_grifts.rb +151 -0
  17. data/KGrift/knewton_grifts/deprecated_graph_and_taxonomy_grifts.rb +353 -0
  18. data/KGrift/knewton_grifts/goal_grifts.rb +203 -0
  19. data/KGrift/knewton_grifts/graph_and_taxonomy_grifts.rb +136 -0
  20. data/KGrift/knewton_grifts/graph_create_grifts.rb +34 -0
  21. data/KGrift/knewton_grifts/graph_query_grifts.rb +448 -0
  22. data/KGrift/knewton_grifts/graph_tools_grifts.rb +151 -0
  23. data/KGrift/knewton_grifts/graph_validation_grifts.rb +447 -0
  24. data/KGrift/knewton_grifts/helper_grifts.rb +92 -0
  25. data/KGrift/knewton_grifts/jmeter_data_grifts.rb +56 -0
  26. data/KGrift/knewton_grifts/learning_instance_grifts.rb +46 -0
  27. data/KGrift/knewton_grifts/looper_grifts.rb +34 -0
  28. data/KGrift/knewton_grifts/moxy_grifts.rb +64 -0
  29. data/KGrift/knewton_grifts/oauth_grifts.rb +182 -0
  30. data/KGrift/knewton_grifts/partner_grifts.rb +70 -0
  31. data/KGrift/knewton_grifts/partner_support_grifts.rb +85 -0
  32. data/KGrift/knewton_grifts/recommendation_setup_grifts.rb +215 -0
  33. data/KGrift/knewton_grifts/registration_grifts.rb +159 -0
  34. data/KGrift/knewton_grifts/registration_info_grifts.rb +23 -0
  35. data/KGrift/knewton_grifts/report_grifts.rb +122 -0
  36. data/KGrift/knewton_grifts/shell_command_grifts.rb +21 -0
  37. data/KGrift/knewton_grifts/student_flow_grifts.rb +560 -0
  38. data/KGrift/knewton_grifts/tag_grifts.rb +41 -0
  39. data/KGrift/knewton_grifts/test_data_grifts.rb +328 -0
  40. data/KGrift/knewton_grifts/test_user_grifts.rb +264 -0
  41. data/KGrift/lib/dtrace.rb +20 -0
  42. data/KGrift/lib/kgrift.rb +7 -0
  43. data/KGrift/test_data_generators/basic_book_and_taxonomies.rb +35 -0
  44. data/KGrift/test_data_generators/lo_test_graph.rb +34 -0
  45. data/KGrift/test_data_generators/partner_owned_book_and_taxonomies.rb +28 -0
  46. data/KGrift/test_data_generators/partner_owned_book_and_taxonomies_unicode.rb +28 -0
  47. data/KGrift/test_data_generators/sandcastle_book_and_taxonomies.rb +13 -0
  48. data/KGrift/test_data_generators/sandcastle_book_and_taxonomies.yml +3709 -0
  49. data/KGrift/test_data_generators/sandcastle_graph.rb +8 -0
  50. data/KGrift/test_data_generators/sandcastle_graph_definition.json +4483 -0
  51. data/KGrift/test_data_generators/sandcastle_graph_full.rb +7 -0
  52. data/KGrift/test_data_generators/sandcastle_taxonomies.yml +378 -0
  53. data/KGrift/test_data_generators/sandcastle_with_taxons.rb +56 -0
  54. data/KGrift/test_data_generators/sandcastle_with_taxons.yml +3994 -0
  55. data/KGrift/test_data_generators/test_users_and_partners.rb +76 -0
  56. data/kgrift.gemspec +43 -0
  57. 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
+