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
@@ -5,6 +5,15 @@
5
5
  # * https://github.com/influitive/apartment/issues/508
6
6
 
7
7
  Rails.application.config.after_initialize do
8
+ next unless defined?(Apartment)
9
+
10
+ # Newer versions of Apartment already solves this issue (and in a better way)
11
+ begin
12
+ require('apartment/version')
13
+ next if Gem::Version.new(Apartment::VERSION) >= Gem::Version.new('2.8.1')
14
+ rescue LoadError
15
+ end
16
+
8
17
  begin
9
18
  Rails.application.eager_load!
10
19
  ActiveRecord::Base.descendants.each do |model|
@@ -14,7 +23,7 @@ Rails.application.config.after_initialize do
14
23
  model.sequence_name = seq_name
15
24
  end
16
25
  end
17
- rescue PG::ConnectionBad, ActiveRecord::NoDatabaseError
26
+ rescue PG::ConnectionBad, ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
18
27
  # no-op if there is no database setup
19
28
  end
20
29
  end
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ CanvasSync::Engine.routes.draw do
2
+ namespace "api" do
3
+ namespace "v1" do
4
+ post '/live_event' => 'live_events#process_event'
5
+ end
6
+ end
7
+ end
@@ -1,35 +1,16 @@
1
- if Rails.version.to_f >= 5.0
2
- class CreateCanvasSyncJobLog < ActiveRecord::Migration[Rails.version.to_f]
3
- def change
4
- create_table :canvas_sync_job_logs do |t|
5
- t.datetime :started_at
6
- t.datetime :completed_at
7
- t.string :exception
8
- t.text :backtrace
9
- t.string :job_class
10
- t.string :status
11
- t.text :metadata
12
- t.text :job_arguments
1
+ class CreateCanvasSyncJobLog < CanvasSync::MiscHelper::MigrationClass
2
+ def change
3
+ create_table :canvas_sync_job_logs do |t|
4
+ t.datetime :started_at
5
+ t.datetime :completed_at
6
+ t.string :exception
7
+ t.text :backtrace
8
+ t.string :job_class
9
+ t.string :status
10
+ t.text :metadata
11
+ t.text :job_arguments
13
12
 
14
- t.timestamps
15
- end
16
- end
17
- end
18
- else
19
- class CreateCanvasSyncJobLog < ActiveRecord::Migration
20
- def change
21
- create_table :canvas_sync_job_logs do |t|
22
- t.datetime :started_at
23
- t.datetime :completed_at
24
- t.string :exception
25
- t.text :backtrace
26
- t.string :job_class
27
- t.string :status
28
- t.text :metadata
29
- t.text :job_arguments
30
-
31
- t.timestamps
32
- end
13
+ t.timestamps
33
14
  end
34
15
  end
35
16
  end
@@ -1,15 +1,6 @@
1
- if Rails.version.to_f >= 5.0
2
- class AddJobIdToCanvasSyncJobLogs < ActiveRecord::Migration[Rails.version.to_f]
3
- def change
4
- add_column :canvas_sync_job_logs, :job_id, :string
5
- add_index :canvas_sync_job_logs, :job_id
6
- end
7
- end
8
- else
9
- class AddJobIdToCanvasSyncJobLogs < ActiveRecord::Migration
10
- def change
11
- add_column :canvas_sync_job_logs, :job_id, :string
12
- add_index :canvas_sync_job_logs, :job_id
13
- end
1
+ class AddJobIdToCanvasSyncJobLogs < CanvasSync::MiscHelper::MigrationClass
2
+ def change
3
+ add_column :canvas_sync_job_logs, :job_id, :string
4
+ add_index :canvas_sync_job_logs, :job_id
14
5
  end
15
6
  end
@@ -1,13 +1,5 @@
1
- if Rails.version.to_f >= 5.0
2
- class AddForkCountToCanvasSyncJobLogs < ActiveRecord::Migration[Rails.version.to_f]
3
- def change
4
- add_column :canvas_sync_job_logs, :fork_count, :integer
5
- end
6
- end
7
- else
8
- class AddForkCountToCanvasSyncJobLogs < ActiveRecord::Migration
9
- def change
10
- add_column :canvas_sync_job_logs, :fork_count, :integer
11
- end
1
+ class AddForkCountToCanvasSyncJobLogs < CanvasSync::MiscHelper::MigrationClass
2
+ def change
3
+ add_column :canvas_sync_job_logs, :fork_count, :integer
12
4
  end
13
5
  end
@@ -0,0 +1,11 @@
1
+ class CreateCanvasSyncSyncBatches < CanvasSync::MiscHelper::MigrationClass
2
+ def change
3
+ create_table :canvas_sync_sync_batches do |t|
4
+ t.datetime :started_at
5
+ t.datetime :completed_at
6
+ t.string :status
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ class AddFullSyncToCanvasSyncSyncBatch < CanvasSync::MiscHelper::MigrationClass
2
+ def change
3
+ add_column :canvas_sync_sync_batches, :full_sync, :boolean, default: false
4
+ add_column :canvas_sync_sync_batches, :batch_genre, :string
5
+ add_column :canvas_sync_sync_batches, :batch_bid, :string
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module CanvasSync
2
+ # An array that "processes" after so many items are added.
3
+ #
4
+ # Example Usage:
5
+ # batches = CanvasSync::BatchProcessor.new(of: 1000) do |batch|
6
+ # # Process the batch somehow
7
+ # end
8
+ # enumerator_of_some_kind.each { |item| batches << item }
9
+ # batches.flush
10
+ class BatchProcessor
11
+ attr_reader :batch_size
12
+
13
+ def initialize(of: 1000, &blk)
14
+ @batch_size = of
15
+ @block = blk
16
+ @current_batch = []
17
+ end
18
+
19
+ def <<(item)
20
+ @current_batch << item
21
+ process_batch if @current_batch.count >= batch_size
22
+ end
23
+
24
+ def add_all(items)
25
+ items.each do |i|
26
+ self << i
27
+ end
28
+ end
29
+
30
+ def flush
31
+ process_batch if @current_batch.present?
32
+ end
33
+
34
+ protected
35
+
36
+ def process_batch
37
+ @block.call(@current_batch)
38
+ @current_batch = []
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,72 @@
1
+ begin
2
+ require "panda_pal"
3
+ require "panda_pal/concerns/ability_helper"
4
+ rescue LoadError
5
+ end
6
+
7
+ module CanvasSync::Concerns
8
+ module AbilityHelper
9
+ extend ActiveSupport::Concern
10
+
11
+ if defined?(PandaPal::Concerns::AbilityHelper)
12
+ include PandaPal::Concerns::AbilityHelper
13
+ else
14
+ def panda_pal_session
15
+ raise "This feature was moved to PandaPal as of CanvasSync 0.20.0/PandaPal 5.9.9. You should update to PandaPal >= 5.9.9."
16
+ end
17
+ end
18
+
19
+ # Middle Domain
20
+
21
+ def launch_context
22
+ @launch_context ||= begin
23
+ if panda_pal_session.lti_launch_placement == "global_navigation"
24
+ :global
25
+ elsif panda_pal_session.get_lti_cust_param('custom_canvas_course_id').present?
26
+ ::Course.find_by(canvas_id: panda_pal_session.get_lti_cust_param('custom_canvas_course_id'))
27
+ else
28
+ ::Account.find_by(canvas_id: panda_pal_session.get_lti_cust_param('custom_canvas_account_id'))
29
+ end
30
+ end
31
+ end
32
+
33
+ def launch_account
34
+ @launch_account ||= launch_context.respond_to?(:account) ?
35
+ launch_context.account :
36
+ ::Account.find_by(canvas_id: panda_pal_session.get_lti_cust_param('custom_canvas_account_id'))
37
+ end
38
+
39
+ # CanvasSync Domain
40
+
41
+ def canvas_permissions
42
+ panda_pal_session[:canvas_permissions] ||= ::Role.joined_permissions(canvas_roles)
43
+ end
44
+
45
+ def canvas_roles
46
+ @canvas_roles ||= Role.for_labels(panda_pal_session.canvas_role_labels, launch_account)
47
+ end
48
+
49
+ def canvas_root_account_roles
50
+ role_labels = panda_pal_session.canvas_account_role_labels('self')
51
+ ::Role.for_labels(role_labels, ::Account.find_by(canvas_parent_account_id: nil))
52
+ end
53
+
54
+ def canvas_account_roles
55
+ canvas_roles.where(base_role_type: 'AccountMembership')
56
+ end
57
+
58
+ def canvas_course_roles
59
+ canvas_roles.where.not(base_role_type: 'AccountMembership')
60
+ end
61
+
62
+ def canvas_super_user?
63
+ panda_pal_session.cache(:canvas_super_user?) do
64
+ panda_pal_session.canvas_site_admin? || (panda_pal_session.canvas_account_role_labels(:root) & ["AccountAdmin", "Account Admin"]).present?
65
+ end
66
+ end
67
+
68
+ def canvas_user_id
69
+ user&.canvas_id || panda_pal_session.get_lti_cust_param('canvas_user_id')
70
+ end
71
+ end
72
+ end
@@ -11,6 +11,8 @@ module CanvasSync::Concerns
11
11
  extend ActiveSupport::Concern
12
12
  include CanvasSync::Record
13
13
 
14
+ CanvasSync::Record.define_feature self, default: ->{ column_names.include?("ancestry") rescue nil }
15
+
14
16
  included do
15
17
  has_ancestry
16
18
  before_save :relink_ancestry, if: :canvas_parent_account_id_changed?
@@ -0,0 +1,15 @@
1
+ module CanvasSync::Concerns
2
+ module Account
3
+ module Base
4
+ extend ActiveSupport::Concern
5
+
6
+ CanvasSync::Record.define_feature self, default: true
7
+
8
+ class_methods do
9
+ def root_account
10
+ where(canvas_parent_account_id: nil).last
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,7 +1,7 @@
1
1
  module CanvasSync::Concerns
2
2
  module ApiSyncable
3
3
  extend ActiveSupport::Concern
4
- NON_EXISTANT_ERRORS = [Faraday::Error::ResourceNotFound, Footrest::HttpError::NotFound]
4
+ NON_EXISTANT_ERRORS = [Faraday::ResourceNotFound, Footrest::HttpError::NotFound]
5
5
 
6
6
  class_methods do
7
7
  def find_or_fetch(canvas_id, save: false, retries: 1, **kwargs)
@@ -30,27 +30,27 @@ module CanvasSync::Concerns
30
30
  end
31
31
 
32
32
  def bulk_sync_from_api_result(api_array, conflict_target: :canvas_id, import_args: {}, all_pages: true, batch_size: 1000)
33
- columns = api_sync_options.keys
33
+ columns = api_sync_options[:field_map].keys
34
34
 
35
35
  update_conditions = {
36
- condition: Importers::BulkImporter.condition_sql(self, columns),
36
+ condition: CanvasSync::Importers::BulkImporter.condition_sql(self, columns),
37
37
  columns: columns,
38
38
  }
39
39
  update_conditions[:conflict_target] = conflict_target if conflict_target.present?
40
40
  options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
41
41
 
42
42
  if all_pages
43
- batcher = BatchProcessor.new(of: batch_size) do |batch|
43
+ batcher = CanvasSync::BatchProcessor.new(of: batch_size) do |batch|
44
44
  import(columns, batch, options)
45
45
  end
46
46
  api_array.all_pages_each do |api_item|
47
- item = new.assign_from_api_params(api_items)
47
+ item = new.assign_from_api_params(api_item)
48
48
  batcher << item
49
49
  end
50
50
  batcher.flush
51
51
  else
52
52
  items = api_array.map do |api_item|
53
- new.assign_from_api_params(api_items)
53
+ new.assign_from_api_params(api_item)
54
54
  end
55
55
  import(columns, batch, options)
56
56
  end
@@ -90,10 +90,12 @@ module CanvasSync::Concerns
90
90
  private
91
91
 
92
92
  def api_sync_race_create!(inst, save: true)
93
- inst = find_or_initialize_by(canvas_id: inst) unless inst.is_a?(self)
94
- yield inst
95
- inst.save! if save && inst.changed?
96
- inst
93
+ transaction(requires_new: true) do
94
+ inst = find_or_initialize_by(canvas_id: inst) unless inst.is_a?(self)
95
+ yield inst
96
+ inst.save! if save && inst.changed?
97
+ inst
98
+ end
97
99
  rescue ActiveRecord::RecordNotUnique
98
100
  inst = find_by(canvas_id: inst.canvas_id)
99
101
  yield inst
@@ -155,6 +157,11 @@ module CanvasSync::Concerns
155
157
  end
156
158
 
157
159
  apply_block = options[:process_response]
160
+
161
+ if self.class.column_names.include?("canvas_synced_at")
162
+ self.canvas_synced_at = mapped_params[:canvas_synced_at] = DateTime.now
163
+ end
164
+
158
165
  if apply_block.present?
159
166
  case apply_block.arity
160
167
  when 1
@@ -0,0 +1,46 @@
1
+
2
+ module CanvasSync::Concerns
3
+ module LiveEventSync
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ define_model_callbacks :process_live_event
8
+ end
9
+
10
+ class_methods do
11
+ def cs_internal_process_live_event(event)
12
+ meta = event[:metadata]
13
+ payload = event[:payload] || event[:body]
14
+
15
+ canvas_id = payload[:id] || payload[:"#{name.underscore}_id"]
16
+ inst = self.find_or_initialize_by(canvas_id: canvas_id)
17
+ model, _, subtype = meta[:event_name].rpartition('_')
18
+
19
+ result = inst.run_callbacks(:process_live_event) do
20
+ inst.process_live_event(subtype.to_sym, payload, meta)
21
+ end
22
+
23
+ inst.save! if result != false && inst.changed?
24
+ end
25
+ end
26
+
27
+ def process_live_event(event_type, payload, metadata)
28
+ api_response = request_from_api
29
+ assign_from_api_params(api_response)
30
+ end
31
+ end
32
+
33
+ CanvasSync::LiveEvents.listen do |event|
34
+ meta = event[:metadata]
35
+ payload = event[:body]
36
+
37
+ event_type = meta[:event_name]
38
+
39
+ model, _, subtype = event_type.rpartition('_')
40
+ mcls = model.classify.constantize rescue nil
41
+
42
+ if mcls.present? && mcls < LiveEventSync
43
+ mcls.cs_internal_process_live_event(event)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,57 @@
1
+ module CanvasSync::Concerns
2
+ module Role
3
+ module Base
4
+ extend ActiveSupport::Concern
5
+
6
+ DEFAULT_ROLE_LABELS = %w[TeacherEnrollment TaEnrollment StudentEnrollment DesignerEnrollment ObserverEnrollment].freeze
7
+
8
+ CanvasSync::Record.define_feature self, default: true
9
+
10
+ class_methods do
11
+ def for_labels(labels, account)
12
+ built_ins = []
13
+ labels = labels.split(',') if labels.is_a?(String)
14
+ custom_labels = Array(labels).reject do |l|
15
+ if DEFAULT_ROLE_LABELS.include?(l)
16
+ built_ins << l
17
+ elsif l == 'Account Admin' || l == 'AccountAdmin'
18
+ built_ins << 'AccountMembership'
19
+ else
20
+ next
21
+ end
22
+ true
23
+ end
24
+
25
+ account_ids = Rails.cache.fetch([self.class.name, "AccountAncestry", account.canvas_id], expires_in: 6.hours) do
26
+ if account.respond_to?(:path_ids)
27
+ account.path.pluck(:canvas_id)
28
+ else
29
+ [].tap do |pids|
30
+ acc = account
31
+ loop do
32
+ break unless acc
33
+ pids.unshift(acc.canvas_id)
34
+ acc = acc.canvas_parent
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ where(workflow_state: 'built_in', base_role_type: built_ins)
41
+ .or(where.not(workflow_state: 'built_in').where(label: custom_labels, canvas_account_id: account_ids))
42
+ end
43
+
44
+ def joined_permissions(roles)
45
+ final = {}
46
+ roles.each do |role|
47
+ role.permissions.each do |perm_name, perm|
48
+ final[perm_name] = false if final[perm_name].nil?
49
+ final[perm_name] = true if perm['enabled'] == true
50
+ end
51
+ end
52
+ final
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,120 @@
1
+ module CanvasSync::Concerns
2
+ module SyncMapping
3
+ extend ActiveSupport::Concern
4
+
5
+ def self.mapping_for(model, key = nil)
6
+ model.try(:get_sync_mapping, key) || Mapping.default_mappings[key || Mapping.normalize_model_name(model)]
7
+ end
8
+
9
+ class_methods do
10
+ def sync_mapping(key = nil, reset: false, &blk)
11
+ key ||= Mapping.normalize_model_name(self)
12
+ key = key.to_s
13
+ existing_map = get_sync_mapping(key)
14
+ mapper = Mapping.new(existing_map&.deep_dup || {}.with_indifferent_access)
15
+ mapper.reset_links if reset
16
+ mapper.instance_exec(&blk)
17
+ @sync_mappings[key] = mapper.map_def.freeze
18
+ end
19
+
20
+ def get_sync_mapping(key = nil)
21
+ key ||= Mapping.normalize_model_name(self)
22
+ key = key.to_s
23
+ @sync_mappings ||= {}
24
+ @sync_mappings[key] || superclass.try(:get_sync_mapping, key) || Mapping.default_for(key)
25
+ end
26
+ end
27
+
28
+ class Mapping
29
+ attr_reader :map_def
30
+
31
+ def initialize(map_def, model: nil)
32
+ @model = model
33
+ @map_def = map_def
34
+ @map_def[:conflict_target] ||= []
35
+ @map_def[:columns] ||= {}
36
+ end
37
+
38
+ def self.normalize_model_name(model)
39
+ model = model.name unless model.is_a?(String)
40
+ model.pluralize.underscore
41
+ end
42
+
43
+ def self.default_for(key)
44
+ default_mappings[key]
45
+ end
46
+
47
+ def self.default_mappings
48
+ @mappings ||= begin
49
+ maps = {}
50
+ default_v1_mappings.each do |mname, legacy|
51
+ m = maps[mname] = {}
52
+
53
+ m[:conflict_target] = Array(legacy[:conflict_target]).map(&:to_sym).map do |lct|
54
+ legacy[:report_columns][lct][:database_column_name]
55
+ end
56
+
57
+ m[:columns] = {}
58
+ legacy[:report_columns].each do |rcol, opts|
59
+ m[:columns][opts[:database_column_name]] = opts.except(:database_column_name).merge!(
60
+ report_column: rcol,
61
+ ).freeze
62
+ end
63
+ end
64
+ maps.with_indifferent_access.freeze
65
+ end
66
+ end
67
+
68
+ def self.default_v1_mappings
69
+ @legacy_mappings ||= begin
70
+ mapping = YAML.load_file(File.join(__dir__, '../processors', "model_mappings.yml")).deep_symbolize_keys!
71
+ override_filepath = Rails.root.join("config/canvas_sync_provisioning_mapping.yml")
72
+
73
+ if File.file?(override_filepath)
74
+ override = YAML.load_file(override_filepath).deep_symbolize_keys!
75
+ mapping = mapping.merge(override)
76
+ end
77
+
78
+ mapping.freeze
79
+ end
80
+ end
81
+
82
+ def conflict_target(*columns)
83
+ if columns.count == 0
84
+ @map_def[:conflict_target]
85
+ else
86
+ @map_def[:conflict_target] = columns.flatten.compact
87
+ end
88
+ end
89
+
90
+ def reset_links
91
+ @map_def[:columns] = {}.with_indifferent_access
92
+ end
93
+
94
+ def unlink_column(key)
95
+ @map_def[:columns].delete(key)
96
+ end
97
+
98
+ def link_column(m = {}, type: nil, **kwargs, &blk)
99
+ if m.is_a?(Hash)
100
+ m = m.merge(kwargs)
101
+ raise "Hash should have exactly 1 entry" if m && m.count != 1
102
+ @map_def[:columns][m.values[0]] = {
103
+ report_column: m.keys[0],
104
+ type: type,
105
+ transform: blk,
106
+ }
107
+ elsif m.is_a?(Symbol)
108
+ raise "Unrecognized keyword arguments" if kwargs.present?
109
+ @map_def[:columns][m] = {
110
+ report_column: m,
111
+ type: type,
112
+ transform: blk,
113
+ }
114
+ else
115
+ raise "Cannot handle argument of type #{m.class}"
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,9 +1,25 @@
1
1
  require "rails"
2
+ require 'chronic_duration'
2
3
 
3
4
  module CanvasSync
4
5
  class Engine < ::Rails::Engine
5
6
  isolate_namespace CanvasSync
6
7
 
8
+ initializer "canvas_sync.safe_yaml_classes" do |app|
9
+ app.config.active_record.yaml_column_permitted_classes ||= []
10
+ app.config.active_record.yaml_column_permitted_classes |= [Symbol, ActiveSupport::HashWithIndifferentAccess]
11
+
12
+ Rails.application.config.after_initialize do
13
+ if ActiveRecord::Base.respond_to?(:yaml_column_permitted_classes)
14
+ ActiveRecord::Base.yaml_column_permitted_classes |= app.config.active_record.yaml_column_permitted_classes
15
+ end
16
+ if ActiveRecord.respond_to?(:yaml_column_permitted_classes)
17
+ ActiveRecord.yaml_column_permitted_classes |= app.config.active_record.yaml_column_permitted_classes
18
+ end
19
+ end
20
+ rescue
21
+ end
22
+
7
23
  initializer :append_migrations do |app|
8
24
  config.paths["db/migrate"].expanded.each do |expanded_path|
9
25
  app.config.paths["db/migrate"] << expanded_path
@@ -11,5 +27,69 @@ module CanvasSync
11
27
  # Apartment will modify this, but it doesn't fully support engine migrations, so we'll reset it here
12
28
  ActiveRecord::Migrator.migrations_paths = Rails.application.paths["db/migrate"].to_a
13
29
  end
30
+
31
+ RETENTION_TYPE = {
32
+ type: 'string',
33
+ validate: ->(value, *args, errors:, **kwargs) {
34
+ origExc = ChronicDuration.raise_exceptions
35
+ ChronicDuration.raise_exceptions = true
36
+ begin
37
+ ChronicDuration.parse(value) unless value == nil
38
+ nil
39
+ rescue ChronicDuration.DurationParseError
40
+ errors << "<path> must be nil or a parseable duration"
41
+ ensure
42
+ ChronicDuration.raise_exceptions = origExc
43
+ end
44
+ }
45
+ }
46
+
47
+ initializer 'canvas_sync.global_methods' do
48
+ next if defined?(canvas_sync_client)
49
+
50
+ require 'panda_pal'
51
+
52
+ class ::Object
53
+ def canvas_sync_client(account_id = nil)
54
+ org = PandaPal::Organization.current
55
+ org = org.platform_api(PandaPal::Platform::Canvas) if (PandaPal::Organization.instance_method(:platform_api) rescue nil)
56
+ Bearcat::Client.new(
57
+ prefix: org.canvas_url,
58
+ token: org.canvas_api_token,
59
+ )
60
+ end
61
+ end
62
+ rescue LoadError
63
+ end
64
+
65
+ initializer :integrate_pandapal do
66
+ require 'panda_pal'
67
+
68
+ Rails.application.reloader.to_prepare do
69
+ if PandaPal::Organization.respond_to?(:scheduled_task)
70
+ if PandaPal::Organization.respond_to?(:define_setting)
71
+ PandaPal::Organization.define_setting(:canvas_sync, {
72
+ type: 'Hash',
73
+ required: false,
74
+ properties: {
75
+ job_log_retention: { **RETENTION_TYPE },
76
+ sync_batch_retention: { **RETENTION_TYPE },
77
+ }
78
+ })
79
+ end
80
+
81
+ unless PandaPal::Organization.task_scheduled?(:clean_canvas_sync_logs)
82
+ PandaPal::Organization.scheduled_task '0 0 3 * * *', :clean_canvas_sync_logs do
83
+ job_log_retention = ChronicDuration.parse(settings.dig(:canvas_sync, :job_log_retention) || '3 months', keep_zero: true).seconds.ago
84
+ JobLog.where('updated_at < ?', job_log_retention).delete_all
85
+
86
+ sync_batch_retention = ChronicDuration.parse(settings.dig(:canvas_sync, :sync_batch_retention) || '6 months', keep_zero: true).seconds.ago
87
+ SyncBatch.where('updated_at < ?', sync_batch_retention).delete_all
88
+ end
89
+ end
90
+ end
91
+ end
92
+ rescue LoadError
93
+ end
14
94
  end
15
95
  end
@@ -48,6 +48,7 @@ module CanvasSync
48
48
  models.each do |model|
49
49
  migration_template "migrations/create_#{model}.rb", "db/migrate/create_#{model}.rb"
50
50
  template "models/#{model.singularize}.rb", "app/models/#{model.singularize}.rb"
51
+ rescue
51
52
  end
52
53
  end
53
54
  end