canvas_sync 0.20.5 → 0.21.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -12
  3. data/app/controllers/canvas_sync/api/v1/live_events_controller.rb +122 -0
  4. data/config/routes.rb +7 -0
  5. data/lib/canvas_sync/concerns/live_event_sync.rb +46 -0
  6. data/lib/canvas_sync/generators/install_live_events_generator.rb +0 -1
  7. data/lib/canvas_sync/generators/templates/models/account.rb +1 -0
  8. data/lib/canvas_sync/generators/templates/models/assignment.rb +1 -0
  9. data/lib/canvas_sync/generators/templates/models/assignment_group.rb +1 -0
  10. data/lib/canvas_sync/generators/templates/models/context_module.rb +1 -0
  11. data/lib/canvas_sync/generators/templates/models/context_module_item.rb +1 -0
  12. data/lib/canvas_sync/generators/templates/models/course.rb +8 -0
  13. data/lib/canvas_sync/generators/templates/models/enrollment.rb +8 -0
  14. data/lib/canvas_sync/generators/templates/models/section.rb +7 -0
  15. data/lib/canvas_sync/generators/templates/models/submission.rb +1 -0
  16. data/lib/canvas_sync/generators/templates/models/user.rb +8 -0
  17. data/lib/canvas_sync/generators/templates/services/live_events/assignment_event.rb +1 -1
  18. data/lib/canvas_sync/generators/templates/services/live_events/assignment_group_event.rb +1 -1
  19. data/lib/canvas_sync/generators/templates/services/live_events/course_event.rb +1 -3
  20. data/lib/canvas_sync/generators/templates/services/live_events/course_section_event.rb +1 -1
  21. data/lib/canvas_sync/generators/templates/services/live_events/enrollment_event.rb +1 -1
  22. data/lib/canvas_sync/generators/templates/services/live_events/grade_event.rb +1 -1
  23. data/lib/canvas_sync/generators/templates/services/live_events/module_event.rb +1 -1
  24. data/lib/canvas_sync/generators/templates/services/live_events/module_item_event.rb +1 -1
  25. data/lib/canvas_sync/generators/templates/services/live_events/submission_event.rb +1 -1
  26. data/lib/canvas_sync/generators/templates/services/live_events/syllabus_event.rb +1 -1
  27. data/lib/canvas_sync/generators/templates/services/live_events/user_event.rb +1 -3
  28. data/lib/canvas_sync/{generators/templates/services/live_events/base_event.rb → live_events/base_handler.rb} +6 -10
  29. data/lib/canvas_sync/live_events/process_event_job.rb +25 -0
  30. data/lib/canvas_sync/live_events.rb +38 -0
  31. data/lib/canvas_sync/version.rb +1 -1
  32. data/lib/canvas_sync.rb +1 -0
  33. data/spec/canvas_sync/live_events/live_event_sync_spec.rb +27 -0
  34. data/spec/canvas_sync/live_events/live_events_controller_spec.rb +54 -0
  35. data/spec/canvas_sync/live_events/process_event_job_spec.rb +38 -0
  36. data/spec/dummy/app/models/account.rb +1 -0
  37. data/spec/dummy/app/models/assignment.rb +1 -0
  38. data/spec/dummy/app/models/assignment_group.rb +1 -0
  39. data/spec/dummy/app/models/context_module.rb +1 -0
  40. data/spec/dummy/app/models/context_module_item.rb +1 -0
  41. data/spec/dummy/app/models/course.rb +8 -0
  42. data/spec/dummy/app/models/enrollment.rb +8 -0
  43. data/spec/dummy/app/models/section.rb +7 -0
  44. data/spec/dummy/app/models/submission.rb +1 -0
  45. data/spec/dummy/app/models/user.rb +8 -0
  46. data/spec/dummy/app/services/live_events/assignment_event.rb +1 -1
  47. data/spec/dummy/app/services/live_events/course_event.rb +1 -3
  48. data/spec/dummy/app/services/live_events/course_section_event.rb +1 -1
  49. data/spec/dummy/app/services/live_events/enrollment_event.rb +1 -1
  50. data/spec/dummy/app/services/live_events/grade_event.rb +1 -1
  51. data/spec/dummy/app/services/live_events/module_event.rb +1 -1
  52. data/spec/dummy/app/services/live_events/module_item_event.rb +1 -1
  53. data/spec/dummy/app/services/live_events/submission_event.rb +1 -1
  54. data/spec/dummy/app/services/live_events/syllabus_event.rb +1 -1
  55. data/spec/dummy/app/services/live_events/user_event.rb +1 -3
  56. data/spec/dummy/config/routes.rb +1 -0
  57. metadata +218 -181
  58. data/app/controllers/api/v1/live_events_controller.rb +0 -18
  59. data/lib/canvas_sync/concerns/auto_relations.rb +0 -11
  60. /data/app/controllers/{api → canvas_sync/api}/v1/health_check_controller.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f59e793e475e0b39b8ec22a5d0f27144e6734a83020874c9d45b0d78c48c1c60
4
- data.tar.gz: fd32c923becad3bb3845979edb6344be3974fbe598908ec2ec99697fbe449703
3
+ metadata.gz: f498c3b1d47cc2a0d9b7e766cba55bf3b5aa7e361551ac9fbd30890c59e6f681
4
+ data.tar.gz: 79b1b5e0efd2a7aa0e4026e9b4cbb5bfa666598f854e0d6f8e94d80be160288f
5
5
  SHA512:
6
- metadata.gz: b117ba9091d2da2e6aea6b1277ebd7cf61bdb127f9dc4e8c2ea2d87ade654cc791950af800053095f8cce13dcf99b231f541472e845fe10fdca2021e85b8028f
7
- data.tar.gz: 8d033ae9f1b2fb233d4706d0953bc4ac0d04dd2a7da6d26414d62f700be2c39e3d6a9755703816c763f5dd5b22f6685d0f259f3bfa82453c84b58beff4ec2fb1
6
+ metadata.gz: c2f4e22caa099836185de175d1ced94113f0e4e9f1a56b7c1b60dad10444ea771563b3c2a95bed4a05acd66ca53db070228d3383bd6aa9a5ecd4e97faf4a3791
7
+ data.tar.gz: e33eb81f9acec4602bcbd97434d7ad9744b41fd7d0846e786c3eb496a28b3b41be8d94134d445fe63942bfc1262dcd0abfb7225056284ece4733fc15e52220b8
data/README.md CHANGED
@@ -9,7 +9,7 @@ Add this line to your application's Gemfile:
9
9
  ```ruby
10
10
  gem 'canvas_sync'
11
11
  ```
12
-
12
+ mount CanvasSync::Engine, at: '/canvas_sync'
13
13
  Models and migrations can be installed using the following generator:
14
14
 
15
15
  ```
@@ -50,18 +50,14 @@ Make sure you've setup sidekiq to work properly with ActiveJob as [outlined here
50
50
 
51
51
  If using apartment and sidekiq make sure you include the [apartment-sidekiq](https://github.com/influitive/apartment-sidekiq) gem so that the jobs are run in the correct tenant.
52
52
 
53
- ## Basic Usage
54
-
55
- Your tool must have an `ActiveJob` compatible job queue adapter configured, such as DelayedJob or Sidekiq. Additionally, you must have a method called `canvas_sync_client` defined in an initializer that returns a Bearcat client for the Canvas instance you are syncing against. Example:
53
+ ### Live Events
54
+ if enabling Live Events, the following additional dependencies are required:
55
+ - If using Core/Data Services events: `httparty`, `json-jwt`
56
+ - If using EventsManager/DejaVu events: `symmetric-encryption`
56
57
 
57
- ```ruby
58
- # config/initializers/canvas_sync.rb
59
- def canvas_sync_client
60
- Bearcat::Client.new(token: current_organization.settings[:api_token], prefix: current_organization.settings[:base_url])
61
- end
62
- ```
58
+ ## Basic Usage
63
59
 
64
- (Having the client defined here means the sensitive API token doesn't have to be passed in plain text between jobs.)
60
+ Your tool must have an `ActiveJob` compatible job queue adapter configured, such as DelayedJob or Sidekiq.
65
61
 
66
62
  Once that's done and you've used the generator to create your models and migrations you can run the standard provisioning sync:
67
63
 
@@ -83,6 +79,47 @@ If you pass in the optional `term_scope` the provisioning reports will be run fo
83
79
 
84
80
  Imports are inserted in bulk with [activerecord-import](https://github.com/zdennis/activerecord-import) so they should be very fast.
85
81
 
82
+ ### Live Events
83
+
84
+ Ensure that
85
+ ```ruby
86
+ mount CanvasSync::Engine, at: '/canvas_sync'
87
+ ```
88
+ is added to your `routes.rb`. Configure `DataServices` or `EventsManager` to send events to `https://YOUR_APP/canvas_sync/api/v1/live_event` (if using `DataServices`, event must be signed).
89
+
90
+ Uncomment `include CanvasSync::Concerns::LiveEventSync` and related lines in the appropriate models. (Some models provide some basic hooks to address a "typical" workflow).
91
+
92
+ When Live Events are received, the corresponding model (if present) instance will receive `process_live_event(subtype, payload, metadata)` (where `subtype` is the event name w/o the model name - eg `user_created` => `created`). The default logic is to call `ApiSyncable` and update the model from the Canvas API. `process_live_event` can be overridden directly, or hooked with the usual Rails callbacks system (eg `before_process_live_event`).
93
+
94
+ You can subscribe to Live Events outside of a model context using an intializer like so:
95
+ ```ruby
96
+ CanvasSync::LiveEvents.subscribe(%w[Optional List of Events]) do |event|
97
+ # Your code here
98
+ # Note that this code is _not_ retried if it fails. If you need retries, use this block to trigger another Job.
99
+ end
100
+ ```
101
+
102
+ #### Event Provenance
103
+
104
+ When using `EventsManager` events, events are verified as having come from a legitimate source by use of `SymmetricEncryption` (and thus `PRODUCTION_KEY1` will need to be set correctly when deployed).
105
+
106
+ When using `DataServices`, CanvasSync uses the `DataServices` JWK to authenticate incoming events. CanvasSync is coded to default to the Prod & Beta JWK URL at https://8axpcl50e4.execute-api.us-east-1.amazonaws.com/main/jwks, but this can be overridden with the `DATASERVICES_JWK_URL` ENV variable.
107
+
108
+ Additionally, when `PandaPal` is installed too, use `https://YOUR_APP/canvas_sync/api/v1/live_event?org=ORG_ID` instead. `CanvasSync` will automatically switch to the correct organization and will validate that the event was indeed from the correct Canvas instance. If you are not using `PandaPal`, you'll need to monkey-patch `CanvasSync::Api::V1::LiveEventsController#validate_tenant!`
109
+
110
+ #### Legacy-Style Event Jobs
111
+
112
+ CanvasSync also supports they legacy style of Event Handlers. In this design, properly-named classes are defined in the `::LiveEvents` module, such as (`class LiveEvents::UserCreatedEvent`). Any `ActiveJob` job is compatible, but CanvasSync also provides `CanvasSync::LiveEvents::BaseHandler` as a helpful base class.
113
+
114
+ When present, these jobs will, per event type (eg `user_created`), override the default behavior, meaning that `subscribe` blocks and `process_live_event` and related callbacks will _not_ be called unless you call them. In other words: If you define `class LiveEvents::UserCreatedEvent` and also
115
+ ```ruby
116
+ subscribe(%w[user_created user_updated]) do |event|
117
+ # ...
118
+ end
119
+ ```
120
+ the subscribe block (and User model) will receive `user_updated` events, but not `user_created` events.
121
+
122
+ These jobs can also be generated from template using `bin/rails generate canvas_sync:install_live_events --events users,courses,etc`
86
123
 
87
124
  ## Advanced Usage
88
125
 
@@ -326,9 +363,22 @@ pool.add_jobs([
326
363
 
327
364
  # ...Later
328
365
  CanvasSync::JobBatches::Pool.from_pid(pool_id).cleanup_redis
366
+ ```
329
367
 
368
+ ### Custom Bearcat Instance
369
+ You can define a global `canvas_sync_client` method to return a Bearcat Client instance for CanvasSync to use:
370
+ ```ruby
371
+ # config/initializers/canvas_sync.rb
372
+ def canvas_sync_client
373
+ Bearcat::Client.new(token: current_organization.settings[:api_token], prefix: current_organization.settings[:base_url])
374
+ end
330
375
  ```
331
376
 
377
+ (Having the client defined here means the sensitive API token doesn't have to be passed in plain text between jobs.)
378
+
379
+ This used to be required, but when both CanvasSync and PandaPal are up to date, this is defined automagically.
380
+
381
+
332
382
  ## Legacy Support
333
383
 
334
384
  ### Legacy Mappings
@@ -468,7 +518,9 @@ Re-running the generator when there's been a gem change will give you several ch
468
518
 
469
519
  Additionally, if there have been schema changes to an existing model you may have to run your own migration to bring it up to speed.
470
520
 
471
- If you make updates to the gem please add any upgrade instructions here.
521
+ Also see `CHANGELOG.md`.
522
+
523
+ If you make updates to the gem please add any upgrade instructions to `CHANGELOG.md`.
472
524
 
473
525
  ## Integrating with existing applications
474
526
 
@@ -0,0 +1,122 @@
1
+ module CanvasSync::Api
2
+ module V1
3
+ class LiveEventsController < ActionController::Base
4
+ around_action :switch_tenant
5
+
6
+ def process_event
7
+ if params[:payload].present?
8
+ process_eventsmanager_event
9
+ else
10
+ process_dataservices_event
11
+ end
12
+ rescue => e
13
+ Rails.logger.error("Live Events Error: #{e.message} - #{e.backtrace}")
14
+ render json: { error: "Live Events Error: #{e.message}" }, status: 422
15
+ end
16
+
17
+ private
18
+
19
+ def process_eventsmanager_event
20
+ event = SymmetricEncryption.decrypt(params[:payload])
21
+ event = JSON.parse(event).with_indifferent_access
22
+
23
+ event[:metadata] = event[:attributes]
24
+ event.delete(:attributes)
25
+
26
+ Rails.logger.debug("Processing event type: #{event['metadata']['event_name']}")
27
+ Rails.logger.debug("Payload: #{event}")
28
+
29
+ validate_tenant!(event.dig(:metadata, :root_account_uuid))
30
+ transform_ids!(event)
31
+ event[:via] = "eventsmanager"
32
+
33
+ dispatch_event(event)
34
+
35
+ head :ok
36
+ end
37
+
38
+ def process_dataservices_event
39
+ require "json/jwt"
40
+ require "httparty"
41
+
42
+ event = nil
43
+
44
+ # Only allow unsigned events during devleopment
45
+ if Rails.env.development?
46
+ event = JSON.parse(request.raw_post) rescue nil
47
+ end
48
+
49
+ event ||= JSON::JWT.decode(request.raw_post, dataservices_jwks)
50
+ event = event.with_indifferent_access
51
+
52
+ Rails.logger.debug("Processing event type: #{event['metadata']['event_name']}")
53
+ Rails.logger.debug("Payload: #{event}")
54
+
55
+ validate_tenant!(event.dig(:metadata, :root_account_uuid))
56
+ transform_ids!(event)
57
+ event[:via] = "dataservices"
58
+
59
+ dispatch_event(event)
60
+
61
+ head :ok
62
+ end
63
+
64
+ def dispatch_event(event)
65
+ CanvasSync::LiveEvents::ProcessEventJob.perform_later(event)
66
+ end
67
+
68
+ # Live events will use a canvas global ID (cross shard) for any ID's provided. This method will return the local ID.
69
+ def local_canvas_id(id)
70
+ # TODO: Don't apply this to cross-shard entries
71
+ id.to_i % 10_000_000_000_000
72
+ end
73
+
74
+ def transform_ids!(data)
75
+ transformed = {}
76
+ data.each do |k, v|
77
+ if k.ends_with?("_id") && (v.is_a?(String) || v.is_a?(Numeric))
78
+ transformed["sharded_#{k}"] = v
79
+ transformed[k] = local_canvas_id(v) rescue v
80
+ elsif v.is_a?(Hash)
81
+ transform_ids!(v)
82
+ end
83
+ end
84
+ data.merge!(transformed)
85
+ end
86
+
87
+ def dataservices_jwks
88
+ require "httparty"
89
+
90
+ jwk_json = Rails.cache.fetch("canvas_sync/dataservices_jwks", expires_in: 24.hours) do
91
+ jkws_url = ENV["DATASERVICES_JWK_URL"].presence || "https://8axpcl50e4.execute-api.us-east-1.amazonaws.com/main/jwks"
92
+ response = HTTParty.get(jkws_url)
93
+ JSON.parse(response.body)
94
+ end
95
+
96
+ JSON::JWK::Set.new(jwk_json)
97
+ end
98
+
99
+ def validate_tenant!(event_uuid)
100
+ if defined?(PandaPal)
101
+ root_info = current_organization.root_account_info
102
+ root_uuid = root_info[:uuid]
103
+
104
+ if !root_uuid.present? || root_uuid != event_uuid
105
+ render json: { error: "Invalid Organization/UUID" }, status: 403
106
+ return
107
+ end
108
+ else
109
+ raise "No way to validate LiveEvent is genuinely from the correct Canvas instance! Monkey-patch CanvasSync::Api::V1::LiveEventsController#validate_tenant!"
110
+ end
111
+ end
112
+
113
+ def switch_tenant(&block)
114
+ if defined?(PandaPal) && (org = params[:organization] || params[:org]).present?
115
+ Apartment::Tenant.switch(PandaPal::Organization.find(org).name, &block)
116
+ else
117
+ yield
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
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
@@ -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]
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[:payload]
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
@@ -36,7 +36,6 @@ module CanvasSync
36
36
  template rb_file.to_s, "app/services/live_events/#{File.basename(rb_file)}"
37
37
  end
38
38
  end
39
- template "base_event.rb", "app/services/live_events/base_event.rb"
40
39
  end
41
40
  end
42
41
  end
@@ -4,6 +4,7 @@ class Account < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
6
  # include CanvasSync::Concerns::Account::Ancestry # Add support for the ancestry Gem
7
+ # include CanvasSync::Concerns::LiveEventSync
7
8
 
8
9
  canvas_sync_features :defaults
9
10
 
@@ -3,6 +3,7 @@
3
3
  class Assignment < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
+ # include CanvasSync::Concerns::LiveEventSync
6
7
 
7
8
  canvas_sync_features :defaults
8
9
 
@@ -3,6 +3,7 @@
3
3
  class AssignmentGroup < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
+ # include CanvasSync::Concerns::LiveEventSync
6
7
 
7
8
  canvas_sync_features :defaults
8
9
 
@@ -6,6 +6,7 @@
6
6
  class ContextModule < ApplicationRecord
7
7
  include CanvasSync::Record
8
8
  include CanvasSync::Concerns::ApiSyncable
9
+ # include CanvasSync::Concerns::LiveEventSync
9
10
 
10
11
  canvas_sync_features :defaults
11
12
 
@@ -3,6 +3,7 @@
3
3
  class ContextModuleItem < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
+ # include CanvasSync::Concerns::LiveEventSync
6
7
 
7
8
  canvas_sync_features :defaults
8
9
 
@@ -6,6 +6,14 @@ class Course < ApplicationRecord
6
6
 
7
7
  canvas_sync_features :defaults
8
8
 
9
+ # include CanvasSync::Concerns::LiveEventSync
10
+ # after_process_live_event do
11
+ # if account.nil?
12
+ # acc = Account.new(canvas_id: canvas_account_id)
13
+ # acc.sync_from_api
14
+ # end
15
+ # end
16
+
9
17
  validates :canvas_id, uniqueness: true, presence: true
10
18
  belongs_to :term, foreign_key: :canvas_term_id, primary_key: :canvas_id, optional: true
11
19
  has_many :enrollments, primary_key: :canvas_id, foreign_key: :canvas_course_id
@@ -6,6 +6,14 @@ class Enrollment < ApplicationRecord
6
6
 
7
7
  canvas_sync_features :defaults
8
8
 
9
+ # include CanvasSync::Concerns::LiveEventSync
10
+ # after_process_live_event do
11
+ # if user.nil?
12
+ # u = User.new(canvas_id: canvas_user_id)
13
+ # u.sync_from_api
14
+ # end
15
+ # end
16
+
9
17
  validates :canvas_id, uniqueness: true, presence: true
10
18
  belongs_to :user, primary_key: :canvas_id, foreign_key: :canvas_user_id, optional: true
11
19
  belongs_to :role, primary_key: :canvas_id, foreign_key: :canvas_role_id, optional: true
@@ -6,6 +6,13 @@ class Section < ApplicationRecord
6
6
 
7
7
  canvas_sync_features :defaults
8
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
+
9
16
  validates :canvas_id, uniqueness: true, presence: true
10
17
  belongs_to :course, primary_key: :canvas_id, foreign_key: :canvas_course_id, optional: true
11
18
  has_many :enrollments, primary_key: :canvas_id, foreign_key: :canvas_section_id
@@ -3,6 +3,7 @@
3
3
  class Submission < ApplicationRecord
4
4
  include CanvasSync::Record
5
5
  include CanvasSync::Concerns::ApiSyncable
6
+ # include CanvasSync::Concerns::LiveEventSync
6
7
 
7
8
  canvas_sync_features :defaults
8
9
 
@@ -6,6 +6,14 @@ class User < ApplicationRecord
6
6
 
7
7
  canvas_sync_features :defaults
8
8
 
9
+ # include CanvasSync::Concerns::LiveEventSync
10
+ # around_process_live_event do |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
+
9
17
  validates :canvas_id, uniqueness: true, presence: true
10
18
  has_many :pseudonyms, primary_key: :canvas_id, foreign_key: :canvas_user_id
11
19
  has_many :enrollments, primary_key: :canvas_id, foreign_key: :canvas_user_id
@@ -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
@@ -1,29 +1,25 @@
1
- # <%= autogenerated_event_warning %>
2
-
3
- module LiveEvents
1
+ module CanvasSync::LiveEvents
4
2
  class RetryLiveEventJob < StandardError; end
5
3
 
6
- class BaseEvent < CanvasSync::Job
4
+ # Deprecated
5
+ class BaseHandler < CanvasSync::Job
7
6
  attr_accessor :raw_payload
8
7
  attr_accessor :payload
9
8
  attr_accessor :metadata
10
9
 
11
10
  def perform(event_payload)
12
11
  @raw_payload = event_payload
13
- @metadata = HashWithIndifferentAccess.new(event_payload["attributes"])
12
+ @metadata = HashWithIndifferentAccess.new(event_payload["metadata"])
14
13
  @payload = HashWithIndifferentAccess.new(event_payload["body"])
15
14
  process_with_retry
16
15
  end
17
16
 
17
+ protected
18
+
18
19
  def process
19
20
  raise "process must be implemented in your subclass"
20
21
  end
21
22
 
22
- # Live events will use a canvas global ID (cross shard) for any ID's provided. This method will return the local ID.
23
- def local_canvas_id(id)
24
- id.to_i % 10_000_000_000_000
25
- end
26
-
27
23
  private
28
24
 
29
25
  # Sometimes a creation and update event get sent by Canvas at almost the exact same time. This means
@@ -0,0 +1,25 @@
1
+
2
+ module CanvasSync::LiveEvents
3
+ class ProcessEventJob < ::CanvasSync::Job
4
+ def perform(event)
5
+ event = event.with_indifferent_access
6
+
7
+ # Support Legacy-Style Event Handlers
8
+ begin
9
+ legacy_handler = "LiveEvents::#{event['metadata']['event_name'].camelcase}Event".constantize
10
+ legacy_handler.perform_later(event)
11
+ return
12
+ rescue LoadError
13
+ end
14
+
15
+ handlers = CanvasSync::LiveEvents.registered_handlers
16
+ handlers.each do |h|
17
+ h.call(event)
18
+ rescue => e
19
+ throw e if Rails.env.test?
20
+ Rails.logger.error("LiveEvent Handler raised Error: #{e}")
21
+ Raven.captureException(e) if defined? Raven
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ require_relative './live_events/base_handler.rb'
2
+ require_relative './live_events/process_event_job.rb'
3
+
4
+ module CanvasSync
5
+ module LiveEvents
6
+
7
+ @@registered_handlers = []
8
+
9
+ class << self
10
+ def listen(event_types = nil, &blk)
11
+ if event_types != nil
12
+ blk = wrap_method(blk) do |inner, event|
13
+ meta = event[:metadata]
14
+ payload = event[:payload]
15
+
16
+ if event_types.include?(meta[:event_name])
17
+ inner.call(*args)
18
+ end
19
+ end
20
+ end
21
+
22
+ @@registered_handlers << blk
23
+ end
24
+
25
+ def registered_handlers
26
+ @@registered_handlers
27
+ end
28
+
29
+ private
30
+
31
+ def wrap_method(inner, &outer)
32
+ ->(*args) {
33
+ outer.call(inner, *args)
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end