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.
- checksums.yaml +5 -5
- data/README.md +235 -151
- data/app/controllers/{api → canvas_sync/api}/v1/health_check_controller.rb +1 -1
- data/app/controllers/canvas_sync/api/v1/live_events_controller.rb +122 -0
- data/app/models/canvas_sync/sync_batch.rb +5 -0
- data/config/initializers/apartment.rb +10 -1
- data/config/routes.rb +7 -0
- data/db/migrate/20170915210836_create_canvas_sync_job_log.rb +12 -31
- data/db/migrate/20180725155729_add_job_id_to_canvas_sync_job_logs.rb +4 -13
- data/db/migrate/20190916154829_add_fork_count_to_canvas_sync_job_logs.rb +3 -11
- data/db/migrate/20201018210836_create_canvas_sync_sync_batches.rb +11 -0
- data/db/migrate/20201030210836_add_full_sync_to_canvas_sync_sync_batch.rb +7 -0
- data/lib/canvas_sync/batch_processor.rb +41 -0
- data/lib/canvas_sync/concerns/ability_helper.rb +72 -0
- data/lib/canvas_sync/concerns/account/ancestry.rb +2 -0
- data/lib/canvas_sync/concerns/account/base.rb +15 -0
- data/lib/canvas_sync/concerns/api_syncable.rb +17 -10
- data/lib/canvas_sync/concerns/live_event_sync.rb +46 -0
- data/lib/canvas_sync/concerns/role/base.rb +57 -0
- data/lib/canvas_sync/concerns/sync_mapping.rb +120 -0
- data/lib/canvas_sync/engine.rb +80 -0
- data/lib/canvas_sync/generators/install_generator.rb +1 -0
- data/lib/canvas_sync/generators/install_live_events_generator.rb +0 -1
- data/lib/canvas_sync/generators/templates/migrations/create_content_migrations.rb +24 -0
- data/lib/canvas_sync/generators/templates/migrations/create_course_nicknames.rb +17 -0
- data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +18 -0
- data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
- data/lib/canvas_sync/generators/templates/migrations/create_learning_outcome_results.rb +46 -0
- data/lib/canvas_sync/generators/templates/migrations/create_learning_outcomes.rb +30 -0
- data/lib/canvas_sync/generators/templates/migrations/create_rubric_assessments.rb +31 -0
- data/lib/canvas_sync/generators/templates/migrations/create_rubric_associations.rb +36 -0
- data/lib/canvas_sync/generators/templates/migrations/create_rubrics.rb +38 -0
- data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
- data/lib/canvas_sync/generators/templates/migrations/create_users.rb +0 -1
- data/lib/canvas_sync/generators/templates/models/account.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/admin.rb +2 -0
- data/lib/canvas_sync/generators/templates/models/assignment.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/assignment_group.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/content_migration.rb +12 -0
- data/lib/canvas_sync/generators/templates/models/context_module.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/context_module_item.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/course.rb +11 -0
- data/lib/canvas_sync/generators/templates/models/course_nickname.rb +13 -0
- data/lib/canvas_sync/generators/templates/models/enrollment.rb +14 -0
- data/lib/canvas_sync/generators/templates/models/grading_period.rb +10 -0
- data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
- data/lib/canvas_sync/generators/templates/models/group.rb +2 -0
- data/lib/canvas_sync/generators/templates/models/group_membership.rb +2 -0
- data/lib/canvas_sync/generators/templates/models/learning_outcome.rb +24 -0
- data/lib/canvas_sync/generators/templates/models/learning_outcome_result.rb +48 -0
- data/lib/canvas_sync/generators/templates/models/pseudonym.rb +2 -0
- data/lib/canvas_sync/generators/templates/models/role.rb +2 -0
- data/lib/canvas_sync/generators/templates/models/rubric.rb +29 -0
- data/lib/canvas_sync/generators/templates/models/rubric_assessment.rb +17 -0
- data/lib/canvas_sync/generators/templates/models/rubric_association.rb +14 -0
- data/lib/canvas_sync/generators/templates/models/section.rb +9 -0
- data/lib/canvas_sync/generators/templates/models/submission.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/term.rb +3 -0
- data/lib/canvas_sync/generators/templates/models/user.rb +11 -0
- data/lib/canvas_sync/generators/templates/models/user_observer.rb +13 -0
- data/lib/canvas_sync/generators/templates/services/live_events/assignment_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/assignment_group_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/course_event.rb +1 -3
- data/lib/canvas_sync/generators/templates/services/live_events/course_section_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/enrollment_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/grade_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/module_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/module_item_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/submission_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/syllabus_event.rb +1 -1
- data/lib/canvas_sync/generators/templates/services/live_events/user_event.rb +1 -3
- data/lib/canvas_sync/importers/bulk_importer.rb +138 -31
- data/lib/canvas_sync/job.rb +7 -5
- data/lib/canvas_sync/job_batches/active_job.rb +108 -0
- data/lib/canvas_sync/job_batches/batch.rb +543 -0
- data/lib/canvas_sync/job_batches/callback.rb +149 -0
- data/lib/canvas_sync/job_batches/chain_builder.rb +249 -0
- data/lib/canvas_sync/job_batches/context_hash.rb +159 -0
- data/lib/canvas_sync/job_batches/hier_batch_ids.lua +25 -0
- data/lib/canvas_sync/job_batches/jobs/base_job.rb +7 -0
- data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +22 -0
- data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +170 -0
- data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +22 -0
- data/lib/canvas_sync/job_batches/pool.rb +245 -0
- data/lib/canvas_sync/job_batches/pool_refill.lua +47 -0
- data/lib/canvas_sync/job_batches/redis_model.rb +69 -0
- data/lib/canvas_sync/job_batches/redis_script.rb +163 -0
- data/lib/canvas_sync/job_batches/schedule_callback.lua +14 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/css/styles.less +182 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/batch_tree.js +108 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/batches_assets/js/util.js +2 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/helpers.rb +41 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_batch_tree.erb +6 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_batches_table.erb +44 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_common.erb +13 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_jobs_table.erb +21 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/_pagination.erb +26 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +81 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/batches.erb +23 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/pool.erb +137 -0
- data/lib/canvas_sync/job_batches/sidekiq/web/views/pools.erb +47 -0
- data/lib/canvas_sync/job_batches/sidekiq/web.rb +218 -0
- data/lib/canvas_sync/job_batches/sidekiq.rb +136 -0
- data/lib/canvas_sync/job_batches/status.rb +91 -0
- data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +99 -0
- data/lib/canvas_sync/jobs/canvas_process_waiter.rb +41 -0
- data/lib/canvas_sync/jobs/report_checker.rb +70 -8
- data/lib/canvas_sync/jobs/report_processor_job.rb +4 -7
- data/lib/canvas_sync/jobs/report_starter.rb +34 -20
- data/lib/canvas_sync/jobs/sync_accounts_job.rb +3 -5
- data/lib/canvas_sync/jobs/sync_admins_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_content_migrations_job.rb +20 -0
- data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -4
- data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +16 -50
- data/lib/canvas_sync/jobs/sync_roles_job.rb +2 -5
- data/lib/canvas_sync/jobs/sync_rubric_assessments_job.rb +15 -0
- data/lib/canvas_sync/jobs/sync_rubric_associations_job.rb +15 -0
- data/lib/canvas_sync/jobs/sync_rubrics_job.rb +15 -0
- data/lib/canvas_sync/jobs/sync_simple_table_job.rb +11 -32
- data/lib/canvas_sync/jobs/sync_submissions_job.rb +6 -4
- data/lib/canvas_sync/jobs/sync_terms_job.rb +9 -8
- data/lib/canvas_sync/jobs/term_batches_job.rb +50 -0
- data/lib/canvas_sync/{generators/templates/services/live_events/base_event.rb → live_events/base_handler.rb} +6 -10
- data/lib/canvas_sync/live_events/process_event_job.rb +26 -0
- data/lib/canvas_sync/live_events.rb +38 -0
- data/lib/canvas_sync/misc_helper.rb +63 -0
- data/lib/canvas_sync/processors/assignment_groups_processor.rb +3 -8
- data/lib/canvas_sync/processors/assignments_processor.rb +3 -8
- data/lib/canvas_sync/processors/content_migrations_processor.rb +19 -0
- data/lib/canvas_sync/processors/context_module_items_processor.rb +3 -8
- data/lib/canvas_sync/processors/context_modules_processor.rb +3 -8
- data/lib/canvas_sync/processors/model_mappings.yml +420 -0
- data/lib/canvas_sync/processors/normal_processor.rb +3 -3
- data/lib/canvas_sync/processors/provisioning_report_processor.rb +42 -55
- data/lib/canvas_sync/processors/report_processor.rb +15 -9
- data/lib/canvas_sync/processors/rubric_assessments_processor.rb +19 -0
- data/lib/canvas_sync/processors/rubric_associations_processor.rb +19 -0
- data/lib/canvas_sync/processors/rubrics_processor.rb +19 -0
- data/lib/canvas_sync/processors/submissions_processor.rb +3 -8
- data/lib/canvas_sync/record.rb +103 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +124 -125
- data/spec/canvas_sync/canvas_sync_spec.rb +224 -155
- data/spec/canvas_sync/jobs/canvas_process_waiter_spec.rb +34 -0
- data/spec/canvas_sync/jobs/job_spec.rb +9 -17
- data/spec/canvas_sync/jobs/report_checker_spec.rb +1 -3
- data/spec/canvas_sync/jobs/report_processor_job_spec.rb +0 -3
- data/spec/canvas_sync/jobs/report_starter_spec.rb +19 -28
- data/spec/canvas_sync/jobs/sync_admins_job_spec.rb +1 -4
- data/spec/canvas_sync/jobs/sync_assignment_groups_job_spec.rb +2 -1
- data/spec/canvas_sync/jobs/sync_assignments_job_spec.rb +3 -2
- data/spec/canvas_sync/jobs/sync_content_migrations_job_spec.rb +30 -0
- data/spec/canvas_sync/jobs/sync_context_module_items_job_spec.rb +3 -2
- data/spec/canvas_sync/jobs/sync_context_modules_job_spec.rb +3 -2
- data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +7 -41
- data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +1 -4
- data/spec/canvas_sync/jobs/sync_simple_table_job_spec.rb +5 -12
- data/spec/canvas_sync/jobs/sync_submissions_job_spec.rb +8 -2
- data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +1 -4
- data/spec/canvas_sync/live_events/live_event_sync_spec.rb +27 -0
- data/spec/canvas_sync/live_events/live_events_controller_spec.rb +54 -0
- data/spec/canvas_sync/live_events/process_event_job_spec.rb +38 -0
- data/spec/canvas_sync/misc_helper_spec.rb +58 -0
- data/spec/canvas_sync/models/assignment_spec.rb +1 -1
- data/spec/canvas_sync/processors/content_migrations_processor_spec.rb +13 -0
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +101 -1
- data/spec/canvas_sync/processors/rubric_assessments_spec.rb +16 -0
- data/spec/canvas_sync/processors/rubric_associations_spec.rb +16 -0
- data/spec/canvas_sync/processors/rubrics_processor_spec.rb +17 -0
- data/spec/dummy/app/models/account.rb +6 -0
- data/spec/dummy/app/models/admin.rb +2 -0
- data/spec/dummy/app/models/assignment.rb +3 -0
- data/spec/dummy/app/models/assignment_group.rb +3 -0
- data/spec/dummy/app/models/content_migration.rb +18 -0
- data/spec/dummy/app/models/context_module.rb +3 -0
- data/spec/dummy/app/models/context_module_item.rb +3 -0
- data/spec/dummy/app/models/course.rb +11 -0
- data/spec/dummy/app/models/course_nickname.rb +19 -0
- data/spec/dummy/app/models/enrollment.rb +14 -0
- data/spec/dummy/app/models/grading_period.rb +16 -0
- data/spec/dummy/app/models/grading_period_group.rb +15 -0
- data/spec/dummy/app/models/group.rb +2 -0
- data/spec/dummy/app/models/group_membership.rb +2 -0
- data/spec/dummy/app/models/learning_outcome.rb +30 -0
- data/spec/dummy/app/models/learning_outcome_result.rb +54 -0
- data/spec/dummy/app/models/pseudonym.rb +16 -0
- data/spec/dummy/app/models/role.rb +2 -0
- data/spec/dummy/app/models/rubric.rb +35 -0
- data/spec/dummy/app/models/rubric_assessment.rb +22 -0
- data/spec/dummy/app/models/rubric_association.rb +20 -0
- data/spec/dummy/app/models/section.rb +9 -0
- data/spec/dummy/app/models/submission.rb +4 -0
- data/spec/dummy/app/models/term.rb +3 -0
- data/spec/dummy/app/models/user.rb +11 -0
- data/spec/dummy/app/models/user_observer.rb +19 -0
- data/spec/dummy/app/services/live_events/assignment_event.rb +1 -1
- data/spec/dummy/app/services/live_events/course_event.rb +1 -3
- data/spec/dummy/app/services/live_events/course_section_event.rb +1 -1
- data/spec/dummy/app/services/live_events/enrollment_event.rb +1 -1
- data/spec/dummy/app/services/live_events/grade_event.rb +1 -1
- data/spec/dummy/app/services/live_events/module_event.rb +1 -1
- data/spec/dummy/app/services/live_events/module_item_event.rb +1 -1
- data/spec/dummy/app/services/live_events/submission_event.rb +1 -1
- data/spec/dummy/app/services/live_events/syllabus_event.rb +1 -1
- data/spec/dummy/app/services/live_events/user_event.rb +1 -3
- data/spec/dummy/config/environments/test.rb +2 -0
- data/spec/dummy/config/routes.rb +1 -0
- data/spec/dummy/db/migrate/20201016181346_create_pseudonyms.rb +24 -0
- data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
- data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
- data/spec/dummy/db/migrate/20211001184920_create_grading_period_groups.rb +24 -0
- data/spec/dummy/db/migrate/20220308072643_create_content_migrations.rb +30 -0
- data/spec/dummy/db/migrate/20220712210559_create_learning_outcomes.rb +36 -0
- data/spec/dummy/db/migrate/{20190702203620_create_users.rb → 20220926221926_create_users.rb} +0 -1
- data/spec/dummy/db/migrate/20240408223326_create_course_nicknames.rb +23 -0
- data/spec/dummy/db/migrate/20240509105100_create_rubrics.rb +44 -0
- data/spec/dummy/db/migrate/20240510094100_create_rubric_associations.rb +42 -0
- data/spec/dummy/db/migrate/20240510101100_create_rubric_assessments.rb +37 -0
- data/spec/dummy/db/migrate/20240523101010_create_learning_outcome_results.rb +52 -0
- data/spec/dummy/db/schema.rb +244 -5
- data/spec/factories/user_factory.rb +0 -1
- data/spec/job_batching/active_job_spec.rb +107 -0
- data/spec/job_batching/batch_spec.rb +489 -0
- data/spec/job_batching/callback_spec.rb +38 -0
- data/spec/job_batching/context_hash_spec.rb +54 -0
- data/spec/job_batching/flow_spec.rb +82 -0
- data/spec/job_batching/integration/fail_then_succeed.rb +42 -0
- data/spec/job_batching/integration/integration.rb +57 -0
- data/spec/job_batching/integration/nested.rb +88 -0
- data/spec/job_batching/integration/simple.rb +47 -0
- data/spec/job_batching/integration/workflow.rb +134 -0
- data/spec/job_batching/integration_helper.rb +50 -0
- data/spec/job_batching/pool_spec.rb +161 -0
- data/spec/job_batching/sidekiq_spec.rb +125 -0
- data/spec/job_batching/status_spec.rb +76 -0
- data/spec/job_batching/support/base_job.rb +14 -0
- data/spec/job_batching/support/sample_callback.rb +2 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/fixtures/reports/content_migrations.csv +3 -0
- data/spec/support/fixtures/reports/course_nicknames.csv +3 -0
- data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
- data/spec/support/fixtures/reports/grading_periods.csv +3 -0
- data/spec/support/fixtures/reports/learning_outcome_results.csv +3 -0
- data/spec/support/fixtures/reports/learning_outcomes.csv +3 -0
- data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
- data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
- data/spec/support/fixtures/reports/rubric_assessments.csv +3 -0
- data/spec/support/fixtures/reports/rubric_associations.csv +3 -0
- data/spec/support/fixtures/reports/rubrics.csv +3 -0
- data/spec/support/fixtures/reports/user_observers.csv +3 -0
- data/spec/support/fixtures/reports/users.csv +3 -2
- data/spec/support/fixtures/reports/xlist.csv +1 -1
- metadata +329 -27
- data/app/controllers/api/v1/live_events_controller.rb +0 -18
- data/lib/canvas_sync/job_chain.rb +0 -57
- data/lib/canvas_sync/jobs/fork_gather.rb +0 -59
- 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 AssignmentGroupEvent < LiveEvents::
|
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::
|
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::
|
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 ModuleItemEvent < LiveEvents::
|
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::
|
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,8 +1,7 @@
|
|
1
1
|
# <%= autogenerated_event_warning %>
|
2
2
|
|
3
3
|
module LiveEvents
|
4
|
-
class UserEvent < LiveEvents::
|
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,
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
34
|
-
|
35
|
-
next if row.nil?
|
31
|
+
mapping[db_col] = opts
|
32
|
+
end
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/canvas_sync/job.rb
CHANGED
@@ -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
|
27
|
+
if batch_context&.[](:on_failure)&.present?
|
26
28
|
begin
|
27
|
-
class_name, method =
|
29
|
+
class_name, method = batch_context[:on_failure].split('.')
|
28
30
|
klass = class_name.constantize
|
29
|
-
klass.send(method.to_sym, e,
|
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
|