canvas_sync 0.16.4 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (260) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +235 -151
  3. data/app/controllers/{api → canvas_sync/api}/v1/health_check_controller.rb +1 -1
  4. data/app/controllers/canvas_sync/api/v1/live_events_controller.rb +122 -0
  5. data/app/models/canvas_sync/sync_batch.rb +5 -0
  6. data/config/initializers/apartment.rb +10 -1
  7. data/config/routes.rb +7 -0
  8. data/db/migrate/20170915210836_create_canvas_sync_job_log.rb +12 -31
  9. data/db/migrate/20180725155729_add_job_id_to_canvas_sync_job_logs.rb +4 -13
  10. data/db/migrate/20190916154829_add_fork_count_to_canvas_sync_job_logs.rb +3 -11
  11. data/db/migrate/20201018210836_create_canvas_sync_sync_batches.rb +11 -0
  12. data/db/migrate/20201030210836_add_full_sync_to_canvas_sync_sync_batch.rb +7 -0
  13. data/lib/canvas_sync/batch_processor.rb +41 -0
  14. data/lib/canvas_sync/concerns/ability_helper.rb +72 -0
  15. data/lib/canvas_sync/concerns/account/ancestry.rb +2 -0
  16. data/lib/canvas_sync/concerns/account/base.rb +15 -0
  17. data/lib/canvas_sync/concerns/api_syncable.rb +17 -10
  18. data/lib/canvas_sync/concerns/live_event_sync.rb +46 -0
  19. data/lib/canvas_sync/concerns/role/base.rb +57 -0
  20. data/lib/canvas_sync/concerns/sync_mapping.rb +120 -0
  21. data/lib/canvas_sync/engine.rb +80 -0
  22. data/lib/canvas_sync/generators/install_generator.rb +1 -0
  23. data/lib/canvas_sync/generators/install_live_events_generator.rb +0 -1
  24. data/lib/canvas_sync/generators/templates/migrations/create_content_migrations.rb +24 -0
  25. data/lib/canvas_sync/generators/templates/migrations/create_course_nicknames.rb +17 -0
  26. data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +18 -0
  27. data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
  28. data/lib/canvas_sync/generators/templates/migrations/create_learning_outcome_results.rb +46 -0
  29. data/lib/canvas_sync/generators/templates/migrations/create_learning_outcomes.rb +30 -0
  30. data/lib/canvas_sync/generators/templates/migrations/create_rubric_assessments.rb +31 -0
  31. data/lib/canvas_sync/generators/templates/migrations/create_rubric_associations.rb +36 -0
  32. data/lib/canvas_sync/generators/templates/migrations/create_rubrics.rb +38 -0
  33. data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
  34. data/lib/canvas_sync/generators/templates/migrations/create_users.rb +0 -1
  35. data/lib/canvas_sync/generators/templates/models/account.rb +3 -0
  36. data/lib/canvas_sync/generators/templates/models/admin.rb +2 -0
  37. data/lib/canvas_sync/generators/templates/models/assignment.rb +3 -0
  38. data/lib/canvas_sync/generators/templates/models/assignment_group.rb +3 -0
  39. data/lib/canvas_sync/generators/templates/models/content_migration.rb +12 -0
  40. data/lib/canvas_sync/generators/templates/models/context_module.rb +3 -0
  41. data/lib/canvas_sync/generators/templates/models/context_module_item.rb +3 -0
  42. data/lib/canvas_sync/generators/templates/models/course.rb +11 -0
  43. data/lib/canvas_sync/generators/templates/models/course_nickname.rb +13 -0
  44. data/lib/canvas_sync/generators/templates/models/enrollment.rb +14 -0
  45. data/lib/canvas_sync/generators/templates/models/grading_period.rb +10 -0
  46. data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
  47. data/lib/canvas_sync/generators/templates/models/group.rb +2 -0
  48. data/lib/canvas_sync/generators/templates/models/group_membership.rb +2 -0
  49. data/lib/canvas_sync/generators/templates/models/learning_outcome.rb +24 -0
  50. data/lib/canvas_sync/generators/templates/models/learning_outcome_result.rb +48 -0
  51. data/lib/canvas_sync/generators/templates/models/pseudonym.rb +2 -0
  52. data/lib/canvas_sync/generators/templates/models/role.rb +2 -0
  53. data/lib/canvas_sync/generators/templates/models/rubric.rb +29 -0
  54. data/lib/canvas_sync/generators/templates/models/rubric_assessment.rb +17 -0
  55. data/lib/canvas_sync/generators/templates/models/rubric_association.rb +14 -0
  56. data/lib/canvas_sync/generators/templates/models/section.rb +9 -0
  57. data/lib/canvas_sync/generators/templates/models/submission.rb +3 -0
  58. data/lib/canvas_sync/generators/templates/models/term.rb +3 -0
  59. data/lib/canvas_sync/generators/templates/models/user.rb +11 -0
  60. data/lib/canvas_sync/generators/templates/models/user_observer.rb +13 -0
  61. data/lib/canvas_sync/generators/templates/services/live_events/assignment_event.rb +1 -1
  62. data/lib/canvas_sync/generators/templates/services/live_events/assignment_group_event.rb +1 -1
  63. data/lib/canvas_sync/generators/templates/services/live_events/course_event.rb +1 -3
  64. data/lib/canvas_sync/generators/templates/services/live_events/course_section_event.rb +1 -1
  65. data/lib/canvas_sync/generators/templates/services/live_events/enrollment_event.rb +1 -1
  66. data/lib/canvas_sync/generators/templates/services/live_events/grade_event.rb +1 -1
  67. data/lib/canvas_sync/generators/templates/services/live_events/module_event.rb +1 -1
  68. data/lib/canvas_sync/generators/templates/services/live_events/module_item_event.rb +1 -1
  69. data/lib/canvas_sync/generators/templates/services/live_events/submission_event.rb +1 -1
  70. data/lib/canvas_sync/generators/templates/services/live_events/syllabus_event.rb +1 -1
  71. data/lib/canvas_sync/generators/templates/services/live_events/user_event.rb +1 -3
  72. data/lib/canvas_sync/importers/bulk_importer.rb +138 -31
  73. data/lib/canvas_sync/job.rb +7 -5
  74. data/lib/canvas_sync/job_batches/active_job.rb +108 -0
  75. data/lib/canvas_sync/job_batches/batch.rb +543 -0
  76. data/lib/canvas_sync/job_batches/callback.rb +149 -0
  77. data/lib/canvas_sync/job_batches/chain_builder.rb +249 -0
  78. data/lib/canvas_sync/job_batches/context_hash.rb +159 -0
  79. data/lib/canvas_sync/job_batches/hier_batch_ids.lua +25 -0
  80. data/lib/canvas_sync/job_batches/jobs/base_job.rb +7 -0
  81. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +22 -0
  82. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +170 -0
  83. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +22 -0
  84. data/lib/canvas_sync/job_batches/pool.rb +245 -0
  85. data/lib/canvas_sync/job_batches/pool_refill.lua +47 -0
  86. data/lib/canvas_sync/job_batches/redis_model.rb +69 -0
  87. data/lib/canvas_sync/job_batches/redis_script.rb +163 -0
  88. data/lib/canvas_sync/job_batches/schedule_callback.lua +14 -0
  89. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less +182 -0
  90. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js +108 -0
  91. data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js +2 -0
  92. data/lib/canvas_sync/job_batches/sidekiq/web/helpers.rb +41 -0
  93. data/lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb +6 -0
  94. data/lib/canvas_sync/job_batches/sidekiq/web/views/_batches_table.erb +44 -0
  95. data/lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb +13 -0
  96. data/lib/canvas_sync/job_batches/sidekiq/web/views/_jobs_table.erb +21 -0
  97. data/lib/canvas_sync/job_batches/sidekiq/web/views/_pagination.erb +26 -0
  98. data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +81 -0
  99. data/lib/canvas_sync/job_batches/sidekiq/web/views/batches.erb +23 -0
  100. data/lib/canvas_sync/job_batches/sidekiq/web/views/pool.erb +137 -0
  101. data/lib/canvas_sync/job_batches/sidekiq/web/views/pools.erb +47 -0
  102. data/lib/canvas_sync/job_batches/sidekiq/web.rb +218 -0
  103. data/lib/canvas_sync/job_batches/sidekiq.rb +136 -0
  104. data/lib/canvas_sync/job_batches/status.rb +91 -0
  105. data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +99 -0
  106. data/lib/canvas_sync/jobs/canvas_process_waiter.rb +41 -0
  107. data/lib/canvas_sync/jobs/report_checker.rb +70 -8
  108. data/lib/canvas_sync/jobs/report_processor_job.rb +4 -7
  109. data/lib/canvas_sync/jobs/report_starter.rb +34 -20
  110. data/lib/canvas_sync/jobs/sync_accounts_job.rb +3 -5
  111. data/lib/canvas_sync/jobs/sync_admins_job.rb +2 -4
  112. data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -4
  113. data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -4
  114. data/lib/canvas_sync/jobs/sync_content_migrations_job.rb +20 -0
  115. data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -4
  116. data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -4
  117. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +16 -50
  118. data/lib/canvas_sync/jobs/sync_roles_job.rb +2 -5
  119. data/lib/canvas_sync/jobs/sync_rubric_assessments_job.rb +15 -0
  120. data/lib/canvas_sync/jobs/sync_rubric_associations_job.rb +15 -0
  121. data/lib/canvas_sync/jobs/sync_rubrics_job.rb +15 -0
  122. data/lib/canvas_sync/jobs/sync_simple_table_job.rb +11 -32
  123. data/lib/canvas_sync/jobs/sync_submissions_job.rb +6 -4
  124. data/lib/canvas_sync/jobs/sync_terms_job.rb +9 -8
  125. data/lib/canvas_sync/jobs/term_batches_job.rb +50 -0
  126. data/lib/canvas_sync/{generators/templates/services/live_events/base_event.rb → live_events/base_handler.rb} +6 -10
  127. data/lib/canvas_sync/live_events/process_event_job.rb +26 -0
  128. data/lib/canvas_sync/live_events.rb +38 -0
  129. data/lib/canvas_sync/misc_helper.rb +63 -0
  130. data/lib/canvas_sync/processors/assignment_groups_processor.rb +3 -8
  131. data/lib/canvas_sync/processors/assignments_processor.rb +3 -8
  132. data/lib/canvas_sync/processors/content_migrations_processor.rb +19 -0
  133. data/lib/canvas_sync/processors/context_module_items_processor.rb +3 -8
  134. data/lib/canvas_sync/processors/context_modules_processor.rb +3 -8
  135. data/lib/canvas_sync/processors/model_mappings.yml +420 -0
  136. data/lib/canvas_sync/processors/normal_processor.rb +3 -3
  137. data/lib/canvas_sync/processors/provisioning_report_processor.rb +42 -55
  138. data/lib/canvas_sync/processors/report_processor.rb +15 -9
  139. data/lib/canvas_sync/processors/rubric_assessments_processor.rb +19 -0
  140. data/lib/canvas_sync/processors/rubric_associations_processor.rb +19 -0
  141. data/lib/canvas_sync/processors/rubrics_processor.rb +19 -0
  142. data/lib/canvas_sync/processors/submissions_processor.rb +3 -8
  143. data/lib/canvas_sync/record.rb +103 -0
  144. data/lib/canvas_sync/version.rb +1 -1
  145. data/lib/canvas_sync.rb +124 -125
  146. data/spec/canvas_sync/canvas_sync_spec.rb +224 -155
  147. data/spec/canvas_sync/jobs/canvas_process_waiter_spec.rb +34 -0
  148. data/spec/canvas_sync/jobs/job_spec.rb +9 -17
  149. data/spec/canvas_sync/jobs/report_checker_spec.rb +1 -3
  150. data/spec/canvas_sync/jobs/report_processor_job_spec.rb +0 -3
  151. data/spec/canvas_sync/jobs/report_starter_spec.rb +19 -28
  152. data/spec/canvas_sync/jobs/sync_admins_job_spec.rb +1 -4
  153. data/spec/canvas_sync/jobs/sync_assignment_groups_job_spec.rb +2 -1
  154. data/spec/canvas_sync/jobs/sync_assignments_job_spec.rb +3 -2
  155. data/spec/canvas_sync/jobs/sync_content_migrations_job_spec.rb +30 -0
  156. data/spec/canvas_sync/jobs/sync_context_module_items_job_spec.rb +3 -2
  157. data/spec/canvas_sync/jobs/sync_context_modules_job_spec.rb +3 -2
  158. data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +7 -41
  159. data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +1 -4
  160. data/spec/canvas_sync/jobs/sync_simple_table_job_spec.rb +5 -12
  161. data/spec/canvas_sync/jobs/sync_submissions_job_spec.rb +8 -2
  162. data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +1 -4
  163. data/spec/canvas_sync/live_events/live_event_sync_spec.rb +27 -0
  164. data/spec/canvas_sync/live_events/live_events_controller_spec.rb +54 -0
  165. data/spec/canvas_sync/live_events/process_event_job_spec.rb +38 -0
  166. data/spec/canvas_sync/misc_helper_spec.rb +58 -0
  167. data/spec/canvas_sync/models/assignment_spec.rb +1 -1
  168. data/spec/canvas_sync/processors/content_migrations_processor_spec.rb +13 -0
  169. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +101 -1
  170. data/spec/canvas_sync/processors/rubric_assessments_spec.rb +16 -0
  171. data/spec/canvas_sync/processors/rubric_associations_spec.rb +16 -0
  172. data/spec/canvas_sync/processors/rubrics_processor_spec.rb +17 -0
  173. data/spec/dummy/app/models/account.rb +6 -0
  174. data/spec/dummy/app/models/admin.rb +2 -0
  175. data/spec/dummy/app/models/assignment.rb +3 -0
  176. data/spec/dummy/app/models/assignment_group.rb +3 -0
  177. data/spec/dummy/app/models/content_migration.rb +18 -0
  178. data/spec/dummy/app/models/context_module.rb +3 -0
  179. data/spec/dummy/app/models/context_module_item.rb +3 -0
  180. data/spec/dummy/app/models/course.rb +11 -0
  181. data/spec/dummy/app/models/course_nickname.rb +19 -0
  182. data/spec/dummy/app/models/enrollment.rb +14 -0
  183. data/spec/dummy/app/models/grading_period.rb +16 -0
  184. data/spec/dummy/app/models/grading_period_group.rb +15 -0
  185. data/spec/dummy/app/models/group.rb +2 -0
  186. data/spec/dummy/app/models/group_membership.rb +2 -0
  187. data/spec/dummy/app/models/learning_outcome.rb +30 -0
  188. data/spec/dummy/app/models/learning_outcome_result.rb +54 -0
  189. data/spec/dummy/app/models/pseudonym.rb +16 -0
  190. data/spec/dummy/app/models/role.rb +2 -0
  191. data/spec/dummy/app/models/rubric.rb +35 -0
  192. data/spec/dummy/app/models/rubric_assessment.rb +22 -0
  193. data/spec/dummy/app/models/rubric_association.rb +20 -0
  194. data/spec/dummy/app/models/section.rb +9 -0
  195. data/spec/dummy/app/models/submission.rb +4 -0
  196. data/spec/dummy/app/models/term.rb +3 -0
  197. data/spec/dummy/app/models/user.rb +11 -0
  198. data/spec/dummy/app/models/user_observer.rb +19 -0
  199. data/spec/dummy/app/services/live_events/assignment_event.rb +1 -1
  200. data/spec/dummy/app/services/live_events/course_event.rb +1 -3
  201. data/spec/dummy/app/services/live_events/course_section_event.rb +1 -1
  202. data/spec/dummy/app/services/live_events/enrollment_event.rb +1 -1
  203. data/spec/dummy/app/services/live_events/grade_event.rb +1 -1
  204. data/spec/dummy/app/services/live_events/module_event.rb +1 -1
  205. data/spec/dummy/app/services/live_events/module_item_event.rb +1 -1
  206. data/spec/dummy/app/services/live_events/submission_event.rb +1 -1
  207. data/spec/dummy/app/services/live_events/syllabus_event.rb +1 -1
  208. data/spec/dummy/app/services/live_events/user_event.rb +1 -3
  209. data/spec/dummy/config/environments/test.rb +2 -0
  210. data/spec/dummy/config/routes.rb +1 -0
  211. data/spec/dummy/db/migrate/20201016181346_create_pseudonyms.rb +24 -0
  212. data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
  213. data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
  214. data/spec/dummy/db/migrate/20211001184920_create_grading_period_groups.rb +24 -0
  215. data/spec/dummy/db/migrate/20220308072643_create_content_migrations.rb +30 -0
  216. data/spec/dummy/db/migrate/20220712210559_create_learning_outcomes.rb +36 -0
  217. data/spec/dummy/db/migrate/{20190702203620_create_users.rb → 20220926221926_create_users.rb} +0 -1
  218. data/spec/dummy/db/migrate/20240408223326_create_course_nicknames.rb +23 -0
  219. data/spec/dummy/db/migrate/20240509105100_create_rubrics.rb +44 -0
  220. data/spec/dummy/db/migrate/20240510094100_create_rubric_associations.rb +42 -0
  221. data/spec/dummy/db/migrate/20240510101100_create_rubric_assessments.rb +37 -0
  222. data/spec/dummy/db/migrate/20240523101010_create_learning_outcome_results.rb +52 -0
  223. data/spec/dummy/db/schema.rb +244 -5
  224. data/spec/factories/user_factory.rb +0 -1
  225. data/spec/job_batching/active_job_spec.rb +107 -0
  226. data/spec/job_batching/batch_spec.rb +489 -0
  227. data/spec/job_batching/callback_spec.rb +38 -0
  228. data/spec/job_batching/context_hash_spec.rb +54 -0
  229. data/spec/job_batching/flow_spec.rb +82 -0
  230. data/spec/job_batching/integration/fail_then_succeed.rb +42 -0
  231. data/spec/job_batching/integration/integration.rb +57 -0
  232. data/spec/job_batching/integration/nested.rb +88 -0
  233. data/spec/job_batching/integration/simple.rb +47 -0
  234. data/spec/job_batching/integration/workflow.rb +134 -0
  235. data/spec/job_batching/integration_helper.rb +50 -0
  236. data/spec/job_batching/pool_spec.rb +161 -0
  237. data/spec/job_batching/sidekiq_spec.rb +125 -0
  238. data/spec/job_batching/status_spec.rb +76 -0
  239. data/spec/job_batching/support/base_job.rb +14 -0
  240. data/spec/job_batching/support/sample_callback.rb +2 -0
  241. data/spec/spec_helper.rb +17 -0
  242. data/spec/support/fixtures/reports/content_migrations.csv +3 -0
  243. data/spec/support/fixtures/reports/course_nicknames.csv +3 -0
  244. data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
  245. data/spec/support/fixtures/reports/grading_periods.csv +3 -0
  246. data/spec/support/fixtures/reports/learning_outcome_results.csv +3 -0
  247. data/spec/support/fixtures/reports/learning_outcomes.csv +3 -0
  248. data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
  249. data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
  250. data/spec/support/fixtures/reports/rubric_assessments.csv +3 -0
  251. data/spec/support/fixtures/reports/rubric_associations.csv +3 -0
  252. data/spec/support/fixtures/reports/rubrics.csv +3 -0
  253. data/spec/support/fixtures/reports/user_observers.csv +3 -0
  254. data/spec/support/fixtures/reports/users.csv +3 -2
  255. data/spec/support/fixtures/reports/xlist.csv +1 -1
  256. metadata +329 -27
  257. data/app/controllers/api/v1/live_events_controller.rb +0 -18
  258. data/lib/canvas_sync/job_chain.rb +0 -57
  259. data/lib/canvas_sync/jobs/fork_gather.rb +0 -59
  260. data/spec/canvas_sync/jobs/fork_gather_spec.rb +0 -73
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ddf6a257acf95c92ba99a6d03c9bcdd99ba1a8ff
4
- data.tar.gz: f10e8f91a225462545354a24daaf006a170bd90d
2
+ SHA256:
3
+ metadata.gz: aecd0f3989ecb1b42c9f8c1c83274333eabe316bf05407cc2de7113e14590cb6
4
+ data.tar.gz: 104cbae017ac24e83b5ad357ed381c6af24577917ec1a494ef9456478874eea0
5
5
  SHA512:
6
- metadata.gz: cde3a3a534316a0faa535347a6c71aa63ab46c5cc5d9c21b01512bb623bed7f6b34fc7da38de345fefcf238cc44f0423f849dd08aa8f57f292472838aa6f7d0c
7
- data.tar.gz: 2e1b8217af038166f60afe4032f0f1ec806761fb81d5727aec57540919146335f7a28292e8e81ba87220eb2d687325b8145be798ed6a29b07aaad0cd6e3b56f4
6
+ metadata.gz: 8059b7b6fcadd5c0b8750b705881496dc63bb90a232e9d950618dff0682f5410ea9cc18a84b9a8a22ea4a051262fb2946c0c6c05a39e3a86871a6ee460f75f08
7
+ data.tar.gz: 2d1b85f6564c8916ae3b8a51290df8bf95d75156b2bb4b595466a502d78dd0f7f20260a14914ce482bd89af963b9b89e23207640c6df6a004616fcd9d0f62ff8
data/README.md CHANGED
@@ -9,7 +9,7 @@ Add this line to your application's Gemfile:
9
9
  ```ruby
10
10
  gem 'canvas_sync'
11
11
  ```
12
-
12
+ mount CanvasSync::Engine, at: '/canvas_sync'
13
13
  Models and migrations can be installed using the following generator:
14
14
 
15
15
  ```
@@ -34,6 +34,7 @@ The following custom reports are required for the specified models:
34
34
  - assignment_groups = "Assignment Group Export" (proserv_assignment_group_export_csv)
35
35
  - context_modules = "Professional Services Context Modules Report" (proserv_context_modules_csv)
36
36
  - context_module_items = "Professional Services Context Module Items Report" (proserv_context_module_items_csv)
37
+ - content_migrations = "Professional Services Content Migrations Report" (proserv_content_migrations_csv)
37
38
 
38
39
  ## Prerequisites
39
40
 
@@ -49,18 +50,14 @@ Make sure you've setup sidekiq to work properly with ActiveJob as [outlined here
49
50
 
50
51
  If using apartment and sidekiq make sure you include the [apartment-sidekiq](https://github.com/influitive/apartment-sidekiq) gem so that the jobs are run in the correct tenant.
51
52
 
52
- ## Basic Usage
53
-
54
- Your tool must have an `ActiveJob` compatible job queue adapter configured, such as DelayedJob or Sidekiq. Additionally, you must have a method called `canvas_sync_client` defined in an initializer that returns a Bearcat client for the Canvas instance you are syncing against. Example:
53
+ ### Live Events
54
+ if enabling Live Events, the following additional dependencies are required:
55
+ - If using Core/Data Services events: `httparty`, `json-jwt`
56
+ - If using EventsManager/DejaVu events: `symmetric-encryption`
55
57
 
56
- ```ruby
57
- # config/initializers/canvas_sync.rb
58
- def canvas_sync_client
59
- Bearcat::Client.new(token: current_organization.settings[:api_token], prefix: current_organization.settings[:base_url])
60
- end
61
- ```
58
+ ## Basic Usage
62
59
 
63
- (Having the client defined here means the sensitive API token doesn't have to be passed in plain text between jobs.)
60
+ Your tool must have an `ActiveJob` compatible job queue adapter configured, such as DelayedJob or Sidekiq.
64
61
 
65
62
  Once that's done and you've used the generator to create your models and migrations you can run the standard provisioning sync:
66
63
 
@@ -82,6 +79,47 @@ If you pass in the optional `term_scope` the provisioning reports will be run fo
82
79
 
83
80
  Imports are inserted in bulk with [activerecord-import](https://github.com/zdennis/activerecord-import) so they should be very fast.
84
81
 
82
+ ### Live Events
83
+
84
+ Ensure that
85
+ ```ruby
86
+ mount CanvasSync::Engine, at: '/canvas_sync'
87
+ ```
88
+ is added to your `routes.rb`. Configure `DataServices` or `EventsManager` to send events to `https://YOUR_APP/canvas_sync/api/v1/live_event` (if using `DataServices`, event must be signed).
89
+
90
+ Uncomment `include CanvasSync::Concerns::LiveEventSync` and related lines in the appropriate models. (Some models provide some basic hooks to address a "typical" workflow).
91
+
92
+ When Live Events are received, the corresponding model (if present) instance will receive `process_live_event(subtype, payload, metadata)` (where `subtype` is the event name w/o the model name - eg `user_created` => `created`). The default logic is to call `ApiSyncable` and update the model from the Canvas API. `process_live_event` can be overridden directly, or hooked with the usual Rails callbacks system (eg `before_process_live_event`).
93
+
94
+ You can subscribe to Live Events outside of a model context using an intializer like so:
95
+ ```ruby
96
+ CanvasSync::LiveEvents.subscribe(%w[Optional List of Events]) do |event|
97
+ # Your code here
98
+ # Note that this code is _not_ retried if it fails. If you need retries, use this block to trigger another Job.
99
+ end
100
+ ```
101
+
102
+ #### Event Provenance
103
+
104
+ When using `EventsManager` events, events are verified as having come from a legitimate source by use of `SymmetricEncryption` (and thus `PRODUCTION_KEY1` will need to be set correctly when deployed).
105
+
106
+ When using `DataServices`, CanvasSync uses the `DataServices` JWK to authenticate incoming events. CanvasSync is coded to default to the Prod & Beta JWK URL at https://8axpcl50e4.execute-api.us-east-1.amazonaws.com/main/jwks, but this can be overridden with the `DATASERVICES_JWK_URL` ENV variable.
107
+
108
+ Additionally, when `PandaPal` is installed too, use `https://YOUR_APP/canvas_sync/api/v1/live_event?org=ORG_ID` instead. `CanvasSync` will automatically switch to the correct organization and will validate that the event was indeed from the correct Canvas instance. If you are not using `PandaPal`, you'll need to monkey-patch `CanvasSync::Api::V1::LiveEventsController#validate_tenant!`
109
+
110
+ #### Legacy-Style Event Jobs
111
+
112
+ CanvasSync also supports they legacy style of Event Handlers. In this design, properly-named classes are defined in the `::LiveEvents` module, such as (`class LiveEvents::UserCreatedEvent`). Any `ActiveJob` job is compatible, but CanvasSync also provides `CanvasSync::LiveEvents::BaseHandler` as a helpful base class.
113
+
114
+ When present, these jobs will, per event type (eg `user_created`), override the default behavior, meaning that `subscribe` blocks and `process_live_event` and related callbacks will _not_ be called unless you call them. In other words: If you define `class LiveEvents::UserCreatedEvent` and also
115
+ ```ruby
116
+ subscribe(%w[user_created user_updated]) do |event|
117
+ # ...
118
+ end
119
+ ```
120
+ the subscribe block (and User model) will receive `user_updated` events, but not `user_created` events.
121
+
122
+ These jobs can also be generated from template using `bin/rails generate canvas_sync:install_live_events --events users,courses,etc`
85
123
 
86
124
  ## Advanced Usage
87
125
 
@@ -91,27 +129,58 @@ This gem also helps with syncing and processing other reports if needed. In orde
91
129
  - Integrate your reports with the `ReportStarter`
92
130
  - Tell the gem what jobs to run
93
131
 
132
+ ### `updated_after`
133
+ An `updated_after` param may be passed when triggering a provision or making a chain:
134
+ ```ruby
135
+ CanvasSync.default_provisioning_report_chain(
136
+ %i[list of models to sync], updated_after: false
137
+ )
138
+ ```
139
+ It may be one of the following values:
140
+ * `false` - Will not apply any `updated_after` filtering to the requested reports
141
+ * An ISO-8601 Date - Will pass the supplied date ad the `updated_after` param for the requested reports
142
+ * `true` (Default) - Will use the start date of the last successful sync
143
+
144
+ If `updated_after` is true, CanvasSync will, by default, perform a full sync every other Sunday.
145
+ This logic can be customized by passing `full_sync_every` parameter.
146
+ If you pass a date to `updated_after`, this logic will be disabled unless you explicitly pass a `full_sync_every` parameter.
147
+ `full_sync_every` accepts the following format strings:
148
+ - `15%` - Each sync will have a 15% chance of running a full sync
149
+ - `10 days` - A full sync will be run every 10 days
150
+ - `sunday` - A full sync will run every Sunday
151
+ - `saturday/4` - A full sync will run every fourth Saturday
152
+
153
+ #### Multiple Sync Chains
154
+ If your app uses multiple Sync Chains, you may run into issues with the automatic `updated_after` and `full_sync_every` logic.
155
+ You can fix this by using custom logic or by setting the `batch_genre` parameter when creating the Job Chain. Chains will only
156
+ use chains of the same genre when computing `updated_after` and `full_sync_every`.
157
+
94
158
  ### Extensible chain
95
159
  It is sometimes desired to extend or customize the chain of jobs that are run with CanvasSync.
96
160
  This can be achieved with the following pattern:
97
161
 
98
162
  ```ruby
99
- job_chain = CanvasSync.default_provisioning_report_chain(
163
+ chain = CanvasSync.default_provisioning_report_chain(
100
164
  %i[list of models to sync]
101
165
  )
102
166
 
103
- # CanvasSync forks the chain for each term within a term scope. The ForkGather Job can be used to unfork the chain.
104
- # For example multiple Terms are in the term_scope. CanvasSync syncs Accounts, Terms, and Users (if enabled) and then
105
- # forks the chain to sync other models (eg Course, Sections, etc.) per-Term.
106
- # ForkGather will wait until all the forked chains are complete before continuing.
107
- # TL;DR: Jobs placed after SyncProvisioningReportJob and before ForkGather will run once per Term per Sync;
108
- # Jobs placed before SyncProvisioningReportJob or after ForkGather will run once per Sync
109
- job_chain[:jobs] << { job: CanvasSync::Jobs::ForkGather, options: {} }
167
+ # Add a custom job to the end of the chain.
168
+ chain << { job: CanvasSyncCompleteWorker, args: [job.id], kwargs: { job_id: job.id } }
110
169
 
111
- # Add a custom job to the end of the chain. Custom jobs must accept 2 arguments (job_chain, options) and call CanvasSync.invoke_next(job_chain) when complete
112
- job_chain[:jobs] << { job: CanvasSyncCompleteWorker, options: { job_id: job.id } }
170
+ chain.process!
113
171
 
114
- CanvasSync.invoke_next(job_chain)
172
+ # The chain object provides a fairly extensive API:
173
+ chain.insert({ job: SomeOtherJob, args: [], kwargs: {} }) # Adds the job to the end of the chain
174
+ chain.insert_at(0, { job: SomeOtherJob }) # Adds the job to the beginning of the chain
175
+ chain.insert({ job: SomeOtherJob }, after: 'CanvasSync::Jobs::SyncTermsJob') # Adds the job right after the SyncTermsJob
176
+ chain.insert({ job: SomeOtherJob }, before: 'CanvasSync::Jobs::SyncTermsJob') # Adds the job right before the SyncTermsJob
177
+ chain.insert({ job: SomeOtherJob }, with: 'CanvasSync::Jobs::SyncTermsJob') # Adds the job to be performed concurrently with the SyncTermsJob
178
+
179
+ # Some Jobs (such as the SyncTermsJob) have a sub-chain for, eg, Courses.
180
+ # chain.insert is aware of these sub-chains and will recurse into them when looking for a before:/after:/with: reference
181
+ chain.insert({ job: SomeOtherJob }, after: 'CanvasSync::Jobs::SyncCoursesJob') # Adds the job to be performed after SyncCoursesJob (which is a sub-job of the terms job and is duplicated for each term in the term_scope:)
182
+ # You can also retrieve the sub-chain like so:
183
+ chain.get_sub_chain('CanvasSync::Jobs::SyncTermsJob')
115
184
  ```
116
185
 
117
186
  ### Processor
@@ -130,13 +199,12 @@ end
130
199
 
131
200
  You must implement a job that will enqueue a report starter for your report. (TODO: would be nice to make some sort of builder for this, so you just define the report and its params and then the gem runs it in a pre-defined job.)
132
201
 
133
- Let's say we have a custom Canvas report called "my_really_cool_report_csv". First, we would need to create a job class that will enqueue a report starter. To work with the `CanvasSync` interface, your class must accept 2 parameters: `job_chain`, and `options`.
202
+ Let's say we have a custom Canvas report called "my_really_cool_report_csv". First, we would need to create a job class that will enqueue a report starter.
134
203
 
135
204
  ```ruby
136
205
  class MyReallyCoolReportJob < CanvasSync::Jobs::ReportStarter
137
- def perform(job_chain, options)
206
+ def perform(options)
138
207
  super(
139
- job_chain,
140
208
  'my_really_cool_report_csv', # Report name
141
209
  { "parameters[param1]" => true }, # Report parameters
142
210
  MyCoolProcessor.to_s, # Your processor class as a string
@@ -148,69 +216,41 @@ end
148
216
 
149
217
  You can also see examples in `lib/canvas_sync/jobs/sync_users_job.rb` and `lib/canvas_sync/jobs/sync_provisioning_report.rb`.
150
218
 
151
- ### Start the jobs
219
+ ### Batching
152
220
 
153
- The `CanvasSync.process_jobs` method allows you to pass in a chain of jobs to run. The job chain must be formatted like:
221
+ The provisioning report uses the `CanvasSync::Importers::BulkImporter` class to bulk import rows with the activerecord-import gem. It inserts rows in batches of 10,000 by default. This can be customized by setting the `BULK_IMPORTER_BATCH_SIZE` environment variable.
154
222
 
155
- ```ruby
156
- {
157
- jobs: [
158
- { job: JobClass, options: {} },
159
- { job: JobClass2, options: {} }
160
- ],
161
- global_options: {}
162
- }
163
- ```
223
+ ### Mapping Overrides
164
224
 
165
- Here is an example that runs our new report job first followed by the builtin provisioning job:
225
+ Overrides are useful for two scenarios:
166
226
 
167
- ```ruby
168
- job_chain = {
169
- jobs: [
170
- { job: MyReallyCoolReportJob, options: {} },
171
- { job: CanvasSync::Jobs::SyncProvisioningReportJob, options: { models: ['users', 'courses'] } }
172
- ],
173
- global_options: {}
174
- }
227
+ - You have an existing application where the column names do not match up with what CanvasSync expects
228
+ - You want to sync some other column in the report that CanvasSync is not configured to sync
175
229
 
176
- CanvasSync.process_jobs(job_chain)
177
- ```
230
+ Mappings can be modified by editing the Model class like such:
231
+ ```ruby
232
+ class User < ApplicationRecord
233
+ include CanvasSync::Record
178
234
 
179
- What if you've got some other job that you want run that doesn't deal with a report? No problem! Just make sure you call `CanvasSync.invoke_next` at the end of your job. Example:
235
+ sync_mapping(reset: false) do # `reset: false` is the default
236
+ # The mapping can be totally cleared with `reset: true` in the `sync_mapping` call, or like such:
237
+ reset_links
180
238
 
239
+ # Add a new column:
240
+ link_column :column_in_report => :column_in_database, type: :datetime
181
241
 
182
- ```ruby
183
- class SomeRandomJob < CanvasSync::Job
184
- def perform(job_chain, options)
185
- i_dunno_do_something!
242
+ # If the column name on the report and in the DB are the same, a shorthand can be used:
243
+ link_column :omit_from_final_grade, type: :datetime
186
244
 
187
- CanvasSync.invoke_next(job_chain)
245
+ # If the defaults define a column you don't want synced, you can remove it from the mapping:
246
+ unlink_column :column_in_database
188
247
  end
189
- end
190
248
 
191
- job_chain = {
192
- jobs: [
193
- { job: SomeRandomJob, options: {} },
194
- { job: CanvasSync::Jobs::SyncProvisioningReportJob, options: { models: ['users', 'courses'] } }
195
- ],
196
- global_options: {}
197
- }
198
-
199
- CanvasSync.process_jobs(job_chain)
249
+ # ...
250
+ end
200
251
  ```
201
252
 
202
- ### Batching
203
-
204
- The provisioning report uses the `CanvasSync::Importers::BulkImporter` class to bulk import rows with the activerecord-import gem. It inserts rows in batches of 10,000 by default. This can be customized by setting the `BULK_IMPORTER_BATCH_SIZE` environment variable.
205
-
206
- ### Mapping Overrides
207
-
208
- Overrides are useful for two scenarios:
209
-
210
- - You have an existing application where the column names do not match up with what CanvasSync expects
211
- - You want to sync some other column in the report that CanvasSync is not configured to sync
212
-
213
- In order to create an override, place a file called `canvas_sync_provisioning_mapping.yml` in your Rails `config` directory. Define the tables and columns you want to override using the following format:
253
+ You can also create a file called `canvas_sync_provisioning_mapping.yml` in your Rails `config` directory. However, this approach requires you to re-specify the complete table in order to modify a table. Define the tables and columns you want to override using the following format:
214
254
 
215
255
  ```ruby
216
256
  users:
@@ -257,6 +297,87 @@ class CanvasSyncModel < ApplicationRecord
257
297
  end
258
298
  ```
259
299
 
300
+ ### Job Batching
301
+ CanvasSync adds a `CanvasSync::JobBatches` module. It adds Sidekiq/sidekiq-batch like support for Job Batches.
302
+ It integrates automatically with both Sidekiq and ActiveJob. The API is highly similar to the Sidekiq-batch implementation,
303
+ documentation for which can be found at https://github.com/mperham/sidekiq/wiki/Batches
304
+
305
+ A batch can be created using `Sidekiq::Batch` or `CanvasSync::JobBatching::Batch`.
306
+
307
+ Also see `canvas_sync/jobs/begin_sync_chain_job`, `canvas_sync/Job_batches/jobs/serial_batch_job`, or `canvas_sync/Job_batches/jobs/concurrent_batch_job` for example usage.
308
+
309
+ Example:
310
+ ```ruby
311
+ batch = CanvasSync::JobBatches::Batch.new
312
+ batch.description = "Some Batch" # Optional, but can be useful for debugging
313
+
314
+ batch.on(:complete, "SomeClass.on_complete", kw_param: 1)
315
+ batch.on(:success, "SomeClass.on_success", some_param: 'foo')
316
+
317
+ # Add context to the batch. Can be accessed as batch_context on any jobs within the batch.
318
+ # Nested Batches will have their contexts merged
319
+ batch.context = {
320
+ some_value: 'blah',
321
+ }
322
+
323
+ batch.jobs do
324
+ # Enqueue jobs like normal
325
+ end
326
+ ```
327
+
328
+ #### Job Pools
329
+ A job pool is like a custom Sidekiq Queue. You can add jobs to it and it will empty itself out into one of the actual queues.
330
+ However, it adds some options for tweaking the logic:
331
+ - `concurrency` (default: `nil`) - Define how many jobs from the pool can run at once.
332
+ - `order` (default: `fifo`) - Define how the pool will empty itself
333
+ - `fifo` - First-In First-Out, a traditional queue
334
+ - `lifo` - Last-In First-Out
335
+ - `random` - Pluck and run jobs in random order
336
+ - `priority` - Execute jobs in a priority order (NB: Due to Redis, this priority-random, meaning that items with the same priority will be run in random order, not fifo)
337
+ - `clean_when_empty` (default: `true`) - Automatically clean the pool when it is empty
338
+ - `on_failed_job` (default `:wait`) - If a Job fails, should the pool `:continue` and still enqueue the next job or `:wait` for the job to succeed
339
+
340
+ Example:
341
+ ```ruby
342
+ pool = CanvasSync::JobBatches::Pool.new(concurrency: 4, order: :priority, clean_when_empty: false)
343
+ pool_id = pool.pid
344
+
345
+ # Add a job to the pool
346
+ pool << {
347
+ job: SomeJob, # The Class of a ActiveJob Job or Sidekiq Worker
348
+ args: [1, 2, 3], # Array of params to pass th e Job
349
+ kwargs: {},
350
+ priority: 100, # Only effective if order=:priority, higher is higher
351
+ }
352
+
353
+ # Add many jobs to the pool
354
+ pool.add_jobs([
355
+ {
356
+ job: SomeJob, # The Class of a ActiveJob Job or Sidekiq Worker
357
+ args: [1, 2, 3], # Array of params to pass th e Job
358
+ kwargs: {},
359
+ priority: 100, # Only effective if order=:priority, higher is higher
360
+ },
361
+ # ...
362
+ ])
363
+
364
+ # ...Later
365
+ CanvasSync::JobBatches::Pool.from_pid(pool_id).cleanup_redis
366
+ ```
367
+
368
+ ### Custom Bearcat Instance
369
+ You can define a global `canvas_sync_client` method to return a Bearcat Client instance for CanvasSync to use:
370
+ ```ruby
371
+ # config/initializers/canvas_sync.rb
372
+ def canvas_sync_client
373
+ Bearcat::Client.new(token: current_organization.settings[:api_token], prefix: current_organization.settings[:base_url])
374
+ end
375
+ ```
376
+
377
+ (Having the client defined here means the sensitive API token doesn't have to be passed in plain text between jobs.)
378
+
379
+ This used to be required, but when both CanvasSync and PandaPal are up to date, this is defined automagically.
380
+
260
381
 
261
382
  ## Legacy Support
262
383
 
@@ -297,7 +418,7 @@ If you want your own jobs to also log to the table all you have to do is have yo
297
418
 
298
419
  If you want to be able to utilize the `CanvasSync::JobLog` without `ActiveJob` (so you can get access to `Sidekiq` features that `ActiveJob` doesn't support), then add the following to an initializer in your Rails app:
299
420
 
300
- ```
421
+ ```ruby
301
422
  Sidekiq.configure_server do |config|
302
423
  config.server_middleware do |chain|
303
424
  chain.add CanvasSync::Sidekiq::Middleware
@@ -308,87 +429,26 @@ end
308
429
  ## Syncronize different reports
309
430
  CanvasSync provides the functionality to import data from other reports into an specific table.
310
431
 
311
- This can be achived by using the followin method
312
-
313
- ```ruby
314
- CanvasSync.provisioning_sync(<array of models to sync>, term_scope: <optional term scope>)
315
- CanvasSync
316
- .simple_report_sync(
317
- [
318
- {
319
- report_name: <report name>,
320
- model: <model to sync>,
321
- params: <hash with the require parameters the report needs to sync>
322
- },
323
- {
324
- report_name: <report name>,
325
- model: <model to sync>,
326
- params: <hash with the require parameters the report needs to sync>
327
- },
328
- ...
329
- ],
330
- term_scope: <optional term scope>
331
- )
332
- ```
333
-
334
- Example:
335
-
336
- ```ruby
337
- CanvasSync
338
- .simple_report_sync(
339
- [
340
- {
341
- report_name: 'proservices_provisioning_csv',
342
- model: 'users',
343
- params: {
344
- "parameters[include_deleted]" => true,
345
- "parameters[users]" => true
346
- }
347
- },
348
- {
349
- report_name: 'proservices_provisioning_csv',
350
- model: 'accounts',
351
- params: {
352
- "parameters[include_deleted]" => true,
353
- "parameters[accounts]" => true
354
- }
355
- }
356
- ]
357
- )
358
- ```
359
-
360
- Example with the term_scope active:
432
+ This can be achieved by using the following method
361
433
 
362
434
  ```ruby
363
- CanvasSync
364
- .simple_report_sync(
365
- [
366
- {
367
- report_name: 'proservices_provisioning_csv',
368
- model: 'sections',
369
- params: {
370
- "parameters[include_deleted]" => true,
371
- "parameters[sections]" => true
372
- }
373
- },
374
- {
375
- report_name: 'proservices_provisioning_csv',
376
- model: 'courses',
377
- params: {
378
- "parameters[include_deleted]" => true,
379
- "parameters[courses]" => true
380
- }
381
- }
382
- ],
383
- term_scope: 'active'
384
- )
435
+ chain = CanvasSync.default_provisioning_report_chain
436
+ chain << {
437
+ job: CanvasSync::Jobs::SyncSimpleTableJob,
438
+ options: {
439
+ report_name: <report name>,
440
+ model: <model to sync>,
441
+ params: <hash with the require parameters the report needs to sync>
442
+ },
443
+ }
444
+ chain.process!
385
445
  ```
386
446
 
387
447
  ## Configuration
388
448
 
389
449
  You can configure CanvasSync settings by doing the following:
390
450
 
391
- ```
451
+ ```ruby
392
452
  CanvasSync.configure do |config|
393
453
  config.classes_to_only_log_errors_on << "ClassToOnlyLogErrorsOn"
394
454
  end
@@ -398,6 +458,29 @@ Available config options (if you add more, please update this!):
398
458
 
399
459
  * `config.classes_to_only_log_errors_on` - use this if you are utilizing the `CanvasSync::JobLog` table, but want certain classes to only persist in the `job_logs` table if an error is encountered. This is useful if you've got a very frequently used job that's filling up your database, and only really care about tracking failures.
400
460
 
461
+ ## Global Options
462
+ You can pass in global_options to a job chain. Global options are added to the batch_context and referenced by
463
+ various internal processes.
464
+
465
+ Pass global options into a job chain, using the options param nested in a :global key.
466
+ `options: { global: {...} }`
467
+
468
+ report_timeout (integer): Number of days until a Canvas report should timeout. Default is 1.
469
+ report_compilation_timeout (integer): Number of days until a Canvas report should timeout. Default is 1 hour.
470
+ You can likely pass a float to achieve sub-day timeouts, but not tested.
471
+ report_max_tries (integer): The number of times to attempt a report before giving up. A report is considered failed
472
+ if it has an 'error' status in Canvas or is deleted.
473
+
474
+ This is an example job chain with global options:
475
+ ```ruby
476
+ job_chain = CanvasSync.default_provisioning_report_chain(
477
+ MODELS_TO_SYNC,
478
+ term_scope: :active,
479
+ full_sync_every: 'sunday',
480
+ options: { global: { report_timeout: 2 } }
481
+ )
482
+ ```
483
+
401
484
  ## Handling Job errors
402
485
 
403
486
  If you need custom handling for when a CanvasSync Job fails, you can add an `:on_failure` option to you Job Chain's `:global_options`.
@@ -421,7 +504,6 @@ class CanvasSyncStarterWorker
421
504
  }
422
505
  }
423
506
  )
424
- CanvasSync.invoke_next(job_chain)
425
507
  end
426
508
 
427
509
  def self.handle_canvas_sync_error(error, **options)
@@ -436,7 +518,9 @@ Re-running the generator when there's been a gem change will give you several ch
436
518
 
437
519
  Additionally, if there have been schema changes to an existing model you may have to run your own migration to bring it up to speed.
438
520
 
439
- If you make updates to the gem please add any upgrade instructions here.
521
+ Also see `CHANGELOG.md`.
522
+
523
+ If you make updates to the gem please add any upgrade instructions to `CHANGELOG.md`.
440
524
 
441
525
  ## Integrating with existing applications
442
526
 
@@ -1,4 +1,4 @@
1
- module Api
1
+ module CanvasSync::Api
2
2
  module V1
3
3
  class HealthCheckController < ActionController::Base
4
4
  def ping
@@ -0,0 +1,122 @@
1
+ module CanvasSync::Api
2
+ module V1
3
+ class LiveEventsController < ActionController::Base
4
+ around_action :switch_tenant
5
+
6
+ def process_event
7
+ if params[:payload].present?
8
+ process_eventsmanager_event
9
+ else
10
+ process_dataservices_event
11
+ end
12
+ rescue => e
13
+ Rails.logger.error("Live Events Error: #{e.message} - #{e.backtrace}")
14
+ render json: { error: "Live Events Error: #{e.message}" }, status: 422
15
+ end
16
+
17
+ private
18
+
19
+ def process_eventsmanager_event
20
+ event = SymmetricEncryption.decrypt(params[:payload])
21
+ event = JSON.parse(event).with_indifferent_access
22
+
23
+ event[:metadata] = event[:attributes]
24
+ event.delete(:attributes)
25
+
26
+ Rails.logger.debug("Processing event type: #{event['metadata']['event_name']}")
27
+ Rails.logger.debug("Payload: #{event}")
28
+
29
+ validate_tenant!(event.dig(:metadata, :root_account_uuid))
30
+ transform_ids!(event)
31
+ event[:via] = "eventsmanager"
32
+
33
+ dispatch_event(event)
34
+
35
+ head :ok
36
+ end
37
+
38
+ def process_dataservices_event
39
+ require "json/jwt"
40
+ require "httparty"
41
+
42
+ event = nil
43
+
44
+ # Only allow unsigned events during devleopment
45
+ if Rails.env.development?
46
+ event = JSON.parse(request.raw_post) rescue nil
47
+ end
48
+
49
+ event ||= JSON::JWT.decode(params[:_json], dataservices_jwks)
50
+ event = event.to_h.with_indifferent_access
51
+
52
+ Rails.logger.debug("Processing event type: #{event['metadata']['event_name']}")
53
+ Rails.logger.debug("Payload: #{event}")
54
+
55
+ validate_tenant!(event.dig(:metadata, :root_account_uuid))
56
+ transform_ids!(event)
57
+ event[:via] = "dataservices"
58
+
59
+ dispatch_event(event)
60
+
61
+ head :ok
62
+ end
63
+
64
+ def dispatch_event(event)
65
+ CanvasSync::LiveEvents::ProcessEventJob.perform_later(event)
66
+ end
67
+
68
+ # Live events will use a canvas global ID (cross shard) for any ID's provided. This method will return the local ID.
69
+ def local_canvas_id(id)
70
+ # TODO: Don't apply this to cross-shard entries
71
+ id.to_i % 10_000_000_000_000
72
+ end
73
+
74
+ def transform_ids!(data)
75
+ transformed = {}
76
+ data.each do |k, v|
77
+ if k.ends_with?("_id") && (v.is_a?(String) || v.is_a?(Numeric))
78
+ transformed["sharded_#{k}"] = v
79
+ transformed[k] = local_canvas_id(v) rescue v
80
+ elsif v.is_a?(Hash)
81
+ transform_ids!(v)
82
+ end
83
+ end
84
+ data.merge!(transformed)
85
+ end
86
+
87
+ def dataservices_jwks
88
+ require "httparty"
89
+
90
+ jwk_json = Rails.cache.fetch("canvas_sync/dataservices_jwks", expires_in: 24.hours) do
91
+ jkws_url = ENV["DATASERVICES_JWK_URL"].presence || "https://8axpcl50e4.execute-api.us-east-1.amazonaws.com/main/jwks"
92
+ response = HTTParty.get(jkws_url)
93
+ JSON.parse(response.body)
94
+ end
95
+
96
+ JSON::JWK::Set.new(jwk_json)
97
+ end
98
+
99
+ def validate_tenant!(event_uuid)
100
+ if defined?(PandaPal)
101
+ root_info = current_organization.root_account_info
102
+ root_uuid = root_info[:uuid]
103
+
104
+ if !root_uuid.present? || root_uuid != event_uuid
105
+ render json: { error: "Invalid Organization/UUID" }, status: 403
106
+ return
107
+ end
108
+ else
109
+ raise "No way to validate LiveEvent is genuinely from the correct Canvas instance! Monkey-patch CanvasSync::Api::V1::LiveEventsController#validate_tenant!"
110
+ end
111
+ end
112
+
113
+ def switch_tenant(&block)
114
+ if defined?(PandaPal) && (org = params[:organization] || params[:org]).present?
115
+ Apartment::Tenant.switch(PandaPal::Organization.find(org).name, &block)
116
+ else
117
+ yield
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,5 @@
1
+ module CanvasSync
2
+ class SyncBatch < ApplicationRecord
3
+ serialize :job_arguments, Array
4
+ end
5
+ end