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
@@ -4,6 +4,15 @@ class Section < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
6
 
7
+ canvas_sync_features :defaults
8
+
9
+ # include CanvasSync::Concerns::LiveEventSync
10
+ # after_process_live_event do
11
+ # # A section change could constitute a crosslisting change, which means
12
+ # # we need to make sure all our enrollments are pointing to the correct course
13
+ # enrollments.update_all(canvas_course_id: canvas_course_id)
14
+ # end
15
+
7
16
  validates :canvas_id, uniqueness: true, presence: true
8
17
  belongs_to :course, primary_key: :canvas_id, foreign_key: :canvas_course_id, optional: true
9
18
  has_many :enrollments, primary_key: :canvas_id, foreign_key: :canvas_section_id
@@ -3,6 +3,9 @@
3
3
  class Submission < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
+ # include CanvasSync::Concerns::LiveEventSync
7
+
8
+ canvas_sync_features :defaults
6
9
 
7
10
  validates :canvas_id, uniqueness: true, presence: true
8
11
  belongs_to :assignment, primary_key: :canvas_id, foreign_key: :canvas_assignment_id, optional: true
@@ -4,8 +4,11 @@ class Term < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
6
 
7
+ canvas_sync_features :defaults
8
+
7
9
  validates :canvas_id, uniqueness: true, presence: true
8
10
  has_many :courses, foreign_key: :canvas_term_id, primary_key: :canvas_id
11
+ belongs_to :grading_period_group, primary_key: :canvas_id, foreign_key: :grading_period_group_id, optional: true
9
12
 
10
13
  api_syncable({
11
14
  canvas_id: :id,
@@ -4,6 +4,16 @@ class User < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
6
 
7
+ canvas_sync_features :defaults
8
+
9
+ # include CanvasSync::Concerns::LiveEventSync
10
+ # around_process_live_event do |user, blk|
11
+ # blk.call
12
+ # rescue Footrest::HttpError::Unauthorized => e
13
+ # # This can happen when a new user is created, but hasn't setup a login on Canvas yet.
14
+ # Rails.logger.info("Failed to fetch user #{canvas_user_id}: #{e.backtrace}")
15
+ # end
16
+
7
17
  validates :canvas_id, uniqueness: true, presence: true
8
18
  has_many :pseudonyms, primary_key: :canvas_id, foreign_key: :canvas_user_id
9
19
  has_many :enrollments, primary_key: :canvas_id, foreign_key: :canvas_user_id
@@ -11,6 +21,7 @@ class User < ApplicationRecord
11
21
  has_many :admin_roles, through: :admins, source: :role
12
22
  has_many :submissions, primary_key: :canvas_id, foreign_key: :canvas_user_id
13
23
  has_many :group_memberships, primary_key: :canvas_id, foreign_key: :canvas_user_id
24
+ has_many :rubrics, primary_key: :canvas_id, foreign_key: :canvas_user_id
14
25
 
15
26
  api_syncable({
16
27
  sis_id: :sis_user_id,
@@ -0,0 +1,13 @@
1
+ # <%= autogenerated_model_warning %>
2
+
3
+ class UserObserver < ApplicationRecord
4
+ include CanvasSync::Record
5
+ include CanvasSync::Concerns::ApiSyncable
6
+
7
+ canvas_sync_features :defaults
8
+
9
+ validates :canvas_id, uniqueness: true, presence: true
10
+
11
+ belongs_to :observing_user, primary_key: :canvas_id, foreign_key: :observing_user_id, class_name: 'User', optional: true
12
+ belongs_to :observed_user, primary_key: :canvas_id, foreign_key: :observed_user_id, class_name: 'User', optional: true
13
+ end
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class AssignmentEvent < LiveEvents::BaseEvent
4
+ class AssignmentEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  canvas_assignment_id = local_canvas_id(payload[:assignment_id])
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class AssignmentGroupEvent < LiveEvents::BaseEvent
4
+ class AssignmentGroupEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  canvas_assignment_group_id = local_canvas_id(payload[:assignment_group_id])
@@ -1,8 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class CourseEvent < LiveEvents::BaseEvent
5
-
4
+ class CourseEvent < CanvasSync::LiveEvents::BaseHandler
6
5
  def process
7
6
  course = Course.where(canvas_id: local_canvas_id(payload[:course_id])).first_or_initialize
8
7
  course.canvas_account_id = local_canvas_id(payload[:account_id])
@@ -12,7 +11,6 @@ module LiveEvents
12
11
  end
13
12
  course.sync_from_api
14
13
  end
15
-
16
14
  end
17
15
 
18
16
  class CourseCreatedEvent < LiveEvents::CourseEvent; end
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class CourseSectionEvent < LiveEvents::BaseEvent
4
+ class CourseSectionEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  section = Section.where(canvas_id: local_canvas_id(payload[:course_section_id])).first_or_initialize
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class EnrollmentEvent < LiveEvents::BaseEvent
4
+ class EnrollmentEvent < CanvasSync::LiveEvents::BaseHandler
5
5
  attr_accessor :enrollment
6
6
 
7
7
  def process()
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class GradeEvent < LiveEvents::BaseEvent
4
+ class GradeEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  raise "process must be implemented in your subclass"
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class ModuleEvent < LiveEvents::BaseEvent
4
+ class ModuleEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  return unless payload["context_type"] == "Course"
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class ModuleItemEvent < LiveEvents::BaseEvent
4
+ class ModuleItemEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  context_module_item = ContextModuleItem.find_or_initialize_by(canvas_id: payload["module_item_id"])
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class SubmissionEvent < LiveEvents::BaseEvent
4
+ class SubmissionEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  submission = Submission.where(canvas_id: local_canvas_id(payload["submission_id"])).first_or_initialize
@@ -1,7 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class SyllabusEvent < LiveEvents::BaseEvent
4
+ class SyllabusEvent < CanvasSync::LiveEvents::BaseHandler
5
5
 
6
6
  def process
7
7
  # syllabus = Syllabus.where(course_id: attrs[:course_id]).first_or_initialize
@@ -1,8 +1,7 @@
1
1
  # <%= autogenerated_event_warning %>
2
2
 
3
3
  module LiveEvents
4
- class UserEvent < LiveEvents::BaseEvent
5
-
4
+ class UserEvent < CanvasSync::LiveEvents::BaseHandler
6
5
  def process
7
6
  canvas_user_id = local_canvas_id(payload[:user_id])
8
7
  user = User.where(canvas_id: canvas_user_id).first_or_initialize
@@ -11,7 +10,6 @@ module LiveEvents
11
10
  # This can happen when a new user is created, but hasn't setup a login on Canvas yet.
12
11
  Rails.logger.info("Failed to fetch user #{canvas_user_id}: #{e.backtrace}")
13
12
  end
14
-
15
13
  end
16
14
 
17
15
  class UserCreatedEvent < LiveEvents::UserEvent; end
@@ -23,40 +23,70 @@ module CanvasSync
23
23
  end
24
24
  end
25
25
 
26
- def self.perform_in_batches(report_file_path, mapping, klass, conflict_target, import_args: {})
27
- csv_column_names = mapping.keys
28
- database_column_names = mapping.values.map { |value| value[:database_column_name] }
29
- rows = []
30
- row_ids = {}
31
- database_conflict_column_name = conflict_target ? mapping[conflict_target][:database_column_name] : nil
26
+ def self.perform_in_batches(report_file_path, raw_mapping, klass, conflict_target, import_args: {})
27
+ mapping = {}.with_indifferent_access
28
+ raw_mapping.each do |db_col, opts|
29
+ next if opts[:deprecated] && !klass.column_names.include?(db_col.to_s)
32
30
 
33
- CSV.foreach(report_file_path, headers: true, header_converters: :symbol) do |row|
34
- row = yield(row) if block_given?
35
- next if row.nil?
31
+ mapping[db_col] = opts
32
+ end
36
33
 
37
- if conflict_target
38
- next if row_ids[row[conflict_target]]
39
- row_ids[row[conflict_target]] = true
40
- end
34
+ csv_column_names = mapping.values.map { |value| value[:report_column].to_s }
35
+ database_column_names = mapping.keys
36
+
37
+ conflict_target = Array(conflict_target).map(&:to_s)
38
+ conflict_target_indices = conflict_target.map{|ct| database_column_names.index(ct) }
41
39
 
42
- rows << csv_column_names.map do |column|
43
- if mapping[column][:type].to_sym == :datetime
44
- # TODO: add some timezone config to the mapping.
45
- # In cases where the timestamp or date doesn't include a timezone, you should be able to specify one
46
- DateTime.parse(row[column]).utc rescue nil # rubocop:disable Style/RescueModifier
47
- else
48
- row[column]
40
+ row_ids = {}
41
+ batcher = CanvasSync::BatchProcessor.new(of: batch_size) do |batch|
42
+ row_ids = {}
43
+ perform_import(klass, database_column_names, batch, conflict_target, import_args)
44
+ end
45
+
46
+ row_buffer_out = ->(row) {
47
+ formatted_row = mapping.map do |db_col, col_def|
48
+ value = nil
49
+ value = row[col_def[:report_column]] if col_def[:report_column]
50
+
51
+ if col_def[:type]
52
+ if col_def[:type].to_sym == :datetime
53
+ # TODO: add some timezone config to the mapping.
54
+ # In cases where the timestamp or date doesn't include a timezone, you should be able to specify one
55
+ value = DateTime.parse(value).utc rescue nil # rubocop:disable Style/RescueModifier
56
+ end
49
57
  end
58
+
59
+ value = col_def[:transform].call(value, row) if col_def[:transform]
60
+
61
+ value
50
62
  end
51
63
 
52
- if rows.length >= batch_size
53
- perform_import(klass, database_column_names, rows, database_conflict_column_name, import_args)
54
- rows = []
55
- row_ids = {}
64
+ if conflict_target.present?
65
+ key = conflict_target_indices.map{|ct| formatted_row[ct] }
66
+ next if row_ids[key]
67
+
68
+ row_ids[key] = true
56
69
  end
70
+
71
+ batcher << formatted_row
72
+ }
73
+
74
+ row_buffer = nil
75
+ if defined?(User) && klass == User && csv_column_names.include?('user_id')
76
+ row_buffer = UserRowBuffer.new(&row_buffer_out)
77
+ else
78
+ row_buffer = NullRowBuffer.new(&row_buffer_out)
57
79
  end
58
80
 
59
- perform_import(klass, database_column_names, rows, database_conflict_column_name, import_args)
81
+ CSV.foreach(report_file_path, headers: true, header_converters: :symbol) do |row|
82
+ row = yield(row) if block_given?
83
+ next if row.nil?
84
+
85
+ row_buffer << row
86
+ end
87
+
88
+ row_buffer.flush
89
+ batcher.flush
60
90
  end
61
91
 
62
92
  def self.perform_import(klass, columns, rows, conflict_target, import_args={})
@@ -64,15 +94,35 @@ module CanvasSync
64
94
  columns = columns.dup
65
95
 
66
96
  update_conditions = {
67
- condition: condition_sql(klass, columns),
97
+ condition: condition_sql(klass, columns, import_args[:sync_start_time]),
68
98
  columns: columns
69
99
  }
70
- update_conditions[:conflict_target] = conflict_target if conflict_target
100
+ update_conditions[:conflict_target] = conflict_target if conflict_target.present?
71
101
 
72
102
  options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
73
-
74
103
  options.delete(:on_duplicate_key_update) if options.key?(:on_duplicate_key_ignore)
75
- klass.import(columns, rows, options)
104
+
105
+ result = nil
106
+ callback_env = {
107
+ batch: rows,
108
+ import_result: nil,
109
+ import_options: options,
110
+ }
111
+ ClassCallbackExecutor.run_if_defined(klass, :sync_batch_import, callback_env) do
112
+ result = klass.import(columns, rows, options)
113
+ callback_env[:import_result] = result
114
+
115
+ global_updates = {
116
+ canvas_synced_at: DateTime.now,
117
+ canvas_sync_batch_id: JobBatches::Batch.current_context[:sync_batch_id],
118
+ }
119
+ global_updates.slice!(*klass.column_names.map(&:to_sym))
120
+ if global_updates.present? && result.ids.present?
121
+ klass.where(id: result.ids).update_all(global_updates)
122
+ end
123
+ end
124
+
125
+ result
76
126
  end
77
127
 
78
128
  # This method generates SQL that looks like:
@@ -85,16 +135,73 @@ module CanvasSync
85
135
  # started_at = Time.now
86
136
  # run_the_users_sync!
87
137
  # changed = User.where("updated_at >= ?", started_at)
88
- def self.condition_sql(klass, columns)
138
+ def self.condition_sql(klass, columns, report_start = nil)
89
139
  columns_str = columns.map { |c| "#{klass.quoted_table_name}.#{c}" }.join(", ")
90
140
  excluded_str = columns.map { |c| "EXCLUDED.#{c}" }.join(", ")
91
- "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
141
+ condition_sql = "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
142
+
143
+ if klass.column_names.include?("canvas_synced_at") && report_start
144
+ condition_sql += " AND #{klass.quoted_table_name}.canvas_synced_at < '#{report_start}'"
145
+ elsif klass.column_names.include?("updated_at") && report_start
146
+ condition_sql += " AND #{klass.quoted_table_name}.updated_at < '#{report_start}'"
147
+ end
148
+
149
+ condition_sql
92
150
  end
93
151
 
94
152
  def self.batch_size
95
153
  batch_size = ENV["BULK_IMPORTER_BATCH_SIZE"].to_i
96
154
  batch_size > 0 ? batch_size : DEFAULT_BATCH_SIZE
97
155
  end
156
+
157
+ class RowBuffer
158
+ def initialize(&block)
159
+ @flush_out = block
160
+ @buffered_rows = []
161
+ end
162
+
163
+ def <<(v)
164
+ @buffered_rows << v
165
+ end
166
+
167
+ def flush(value = @buffered_rows)
168
+ if value.is_a?(Array)
169
+ value.each do |v|
170
+ @flush_out.call(v)
171
+ end
172
+ else
173
+ @flush_out.call(value)
174
+ end
175
+ @buffered_rows = []
176
+ end
177
+ end
178
+
179
+ class NullRowBuffer
180
+ def initialize(&block)
181
+ @flush_out = block
182
+ end
183
+
184
+ def <<(v)
185
+ @flush_out.call(v)
186
+ end
187
+
188
+ def flush; end
189
+ end
190
+
191
+ # Ensures that, if a User has multiple rows, one with a SIS ID is preferred.
192
+ # This is mainly to fix issues in legacy apps - the suggested approach for new apps
193
+ # is to sync and use the Pseudonymes table
194
+ class UserRowBuffer < RowBuffer
195
+ def <<(v)
196
+ flush if @buffered_rows[0] && @buffered_rows[0][:canvas_user_id] != v[:canvas_user_id]
197
+ super
198
+ end
199
+
200
+ def flush
201
+ row = @buffered_rows.find{|r| r[:user_id].present? } || @buffered_rows.last
202
+ super(row.present? ? [row] : [])
203
+ end
204
+ end
98
205
  end
99
206
  end
100
207
  end
@@ -3,7 +3,11 @@ require "active_job"
3
3
  module CanvasSync
4
4
  # Inherit from this class to build a Job that will log to the canvas_sync_job_logs table
5
5
  class Job < ActiveJob::Base
6
+ attr_reader :job_log
7
+
6
8
  before_enqueue do |job|
9
+ # Fixes an issue when the Job is enqueued by SidekiqScheduler/Rufus.
10
+ ActiveRecord::Base.connection.verify!
7
11
  create_job_log(job)
8
12
  end
9
13
 
@@ -13,8 +17,6 @@ module CanvasSync
13
17
  @job_log.started_at = Time.now
14
18
  @job_log.save
15
19
 
16
- @job_chain = job.arguments[0] if job.arguments[0].is_a?(Hash) && job.arguments[0].include?(:jobs)
17
-
18
20
  begin
19
21
  block.call
20
22
  @job_log.status = JobLog::SUCCESS_STATUS
@@ -22,11 +24,11 @@ module CanvasSync
22
24
  @job_log.exception = "#{e.class}: #{e.message}"
23
25
  @job_log.backtrace = e.backtrace.join('\n')
24
26
  @job_log.status = JobLog::ERROR_STATUS
25
- if @job_chain&.[](:global_options)&.[](:on_failure)&.present?
27
+ if batch_context&.[](:on_failure)&.present?
26
28
  begin
27
- class_name, method = @job_chain[:global_options][:on_failure].split('.')
29
+ class_name, method = batch_context[:on_failure].split('.')
28
30
  klass = class_name.constantize
29
- klass.send(method.to_sym, e, job_chain: @job_chain, job_log: @job_log)
31
+ klass.send(method.to_sym, e, batch_context: batch_context, job_log: @job_log)
30
32
  rescue => e2
31
33
  @job_log.backtrace += "\n\nError Occurred while handling an Error: #{e2.class}: #{e2.message}"
32
34
  @job_log.backtrace += "\n" + e2.backtrace.join('\n')
@@ -0,0 +1,108 @@
1
+
2
+ module CanvasSync
3
+ module JobBatches
4
+ module ActiveJob
5
+ module BatchAwareJob
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_perform do |job, block|
10
+ if (@bid) # This _must_ be @bid - not just bid
11
+ prev_batch = Thread.current[CURRENT_BATCH_THREAD_KEY]
12
+ begin
13
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = Batch.new(@bid)
14
+ block.call
15
+ Thread.current[CURRENT_BATCH_THREAD_KEY].save_context_changes
16
+ Batch.process_successful_job(@bid, job_id)
17
+ rescue
18
+ Batch.process_failed_job(@bid, job_id)
19
+ raise
20
+ ensure
21
+ Thread.current[CURRENT_BATCH_THREAD_KEY] = prev_batch
22
+ end
23
+ else
24
+ block.call
25
+ end
26
+ end
27
+
28
+ around_enqueue do |job, block|
29
+ if (batch = Thread.current[CURRENT_BATCH_THREAD_KEY])
30
+ @bid = batch.bid
31
+ batch.increment_job_queue(job_id) if @bid
32
+ end
33
+ block.call
34
+ end
35
+ end
36
+
37
+ def bid
38
+ @bid || Thread.current[CURRENT_BATCH_THREAD_KEY]&.bid
39
+ end
40
+
41
+ def batch
42
+ Thread.current[CURRENT_BATCH_THREAD_KEY]
43
+ end
44
+
45
+ def batch_context
46
+ batch&.context || {}
47
+ end
48
+
49
+ def valid_within_batch?
50
+ batch.valid?
51
+ end
52
+
53
+ def serialize
54
+ super.tap do |data|
55
+ data['batch_id'] = @bid # This _must_ be @bid - not just bid
56
+ data
57
+ end
58
+ end
59
+
60
+ def deserialize(data)
61
+ super
62
+ @bid = data['batch_id']
63
+ end
64
+ end
65
+
66
+ class ActiveJobCallbackWorker < ::ActiveJob::Base
67
+ include Batch::Callback::CallbackWorkerCommon
68
+
69
+ def self.enqueue_all(args, queue)
70
+ args.each do |arg_set|
71
+ set(queue: queue).perform_later(*arg_set)
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.handle_job_death(job, error = nil)
77
+ if job.is_a?(Array)
78
+ event = ActiveSupport::Notifications::Event.new(*job)
79
+ payload = event.payload
80
+ job = payload[:job].serialize
81
+ error = payload[:error]
82
+ end
83
+
84
+ if job["job_id"].present? && job["batch_id"].present?
85
+ CanvasSync::JobBatches::Batch.process_dead_job(job['batch_id'], job['job_id'])
86
+ end
87
+ end
88
+
89
+ def self.configure
90
+ ::ActiveJob::Base.include BatchAwareJob
91
+
92
+ begin
93
+ ActiveSupport::Notifications.subscribe "discard.active_job" do |*args|
94
+ handle_job_death(args)
95
+ end
96
+
97
+ ActiveSupport::Notifications.subscribe "retry_stopped.active_job" do |*args|
98
+ handle_job_death(args)
99
+ end
100
+ rescue => err
101
+ Rails.logger.warn(err)
102
+ end
103
+
104
+ Batch::Callback.worker_class ||= ActiveJobCallbackWorker
105
+ end
106
+ end
107
+ end
108
+ end