canvas_sync 0.3.3 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/app/controllers/api/v1/health_check_controller.rb +7 -0
  3. data/app/controllers/api/v1/live_events_controller.rb +16 -0
  4. data/lib/canvas_sync.rb +20 -5
  5. data/lib/canvas_sync/generators/install_generator.rb +1 -1
  6. data/lib/canvas_sync/generators/install_live_events_generator.rb +38 -0
  7. data/lib/canvas_sync/generators/templates/{course.rb → models/course.rb} +0 -0
  8. data/lib/canvas_sync/generators/templates/{create_courses.rb → models/create_courses.rb} +0 -0
  9. data/lib/canvas_sync/generators/templates/{create_enrollments.rb → models/create_enrollments.rb} +0 -0
  10. data/lib/canvas_sync/generators/templates/models/create_roles.rb +17 -0
  11. data/lib/canvas_sync/generators/templates/{create_sections.rb → models/create_sections.rb} +0 -0
  12. data/lib/canvas_sync/generators/templates/{create_terms.rb → models/create_terms.rb} +0 -0
  13. data/lib/canvas_sync/generators/templates/{create_users.rb → models/create_users.rb} +0 -0
  14. data/lib/canvas_sync/generators/templates/{enrollment.rb → models/enrollment.rb} +0 -0
  15. data/lib/canvas_sync/generators/templates/models/role.rb +18 -0
  16. data/lib/canvas_sync/generators/templates/{section.rb → models/section.rb} +0 -0
  17. data/lib/canvas_sync/generators/templates/{term.rb → models/term.rb} +0 -0
  18. data/lib/canvas_sync/generators/templates/{user.rb → models/user.rb} +0 -0
  19. data/lib/canvas_sync/generators/templates/services/live_events/assignment/assignment_created_event.rb +5 -0
  20. data/lib/canvas_sync/generators/templates/services/live_events/assignment/assignment_event.rb +63 -0
  21. data/lib/canvas_sync/generators/templates/services/live_events/assignment/assignment_updated_event.rb +4 -0
  22. data/lib/canvas_sync/generators/templates/services/live_events/base_event.rb +23 -0
  23. data/lib/canvas_sync/generators/templates/services/live_events/course/course_created_event.rb +4 -0
  24. data/lib/canvas_sync/generators/templates/services/live_events/course/course_event.rb +42 -0
  25. data/lib/canvas_sync/generators/templates/services/live_events/course/course_updated_event.rb +4 -0
  26. data/lib/canvas_sync/generators/templates/services/live_events/enrollment/enrollment_created_event.rb +4 -0
  27. data/lib/canvas_sync/generators/templates/services/live_events/enrollment/enrollment_event.rb +58 -0
  28. data/lib/canvas_sync/generators/templates/services/live_events/enrollment/enrollment_updated_event.rb +4 -0
  29. data/lib/canvas_sync/generators/templates/services/live_events/grade/grade_changed_event.rb +4 -0
  30. data/lib/canvas_sync/generators/templates/services/live_events/grade/grade_event.rb +56 -0
  31. data/lib/canvas_sync/generators/templates/services/live_events/submission/submission_created_event.rb +4 -0
  32. data/lib/canvas_sync/generators/templates/services/live_events/submission/submission_event.rb +57 -0
  33. data/lib/canvas_sync/generators/templates/services/live_events/submission/submission_updated_event.rb +4 -0
  34. data/lib/canvas_sync/generators/templates/services/live_events/syllabus/syllabus_event.rb +35 -0
  35. data/lib/canvas_sync/generators/templates/services/live_events/syllabus/syllabus_updated_event.rb +2 -0
  36. data/lib/canvas_sync/generators/templates/services/live_events/user/user_created_event.rb +4 -0
  37. data/lib/canvas_sync/generators/templates/services/live_events/user/user_event.rb +35 -0
  38. data/lib/canvas_sync/generators/templates/services/live_events/user/user_updated_event.rb +4 -0
  39. data/lib/canvas_sync/importers/bulk_importer.rb +1 -1
  40. data/lib/canvas_sync/jobs/sync_roles_job.rb +18 -0
  41. data/lib/canvas_sync/version.rb +1 -1
  42. data/spec/canvas_sync/canvas_sync_spec.rb +28 -0
  43. data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +16 -0
  44. data/spec/canvas_sync/models/roles_spec.rb +46 -0
  45. data/spec/dummy/app/models/role.rb +24 -0
  46. data/spec/dummy/config/database.yml +4 -4
  47. data/spec/dummy/db/migrate/20180103162102_create_roles.rb +23 -0
  48. data/spec/dummy/db/schema.rb +16 -1
  49. data/spec/factories/role_factory.rb +8 -0
  50. data/spec/support/fake_canvas.rb +4 -0
  51. data/spec/support/fixtures/canvas_responses/roles.json +748 -0
  52. metadata +52 -14
@@ -0,0 +1,56 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::GradeEvent < LiveEvents::BaseEvent
4
+ # The following is provided in the live events call:
5
+ # {
6
+ # submission_id: submission.global_id,
7
+ # assignment_id: submission.global_assignment_id,
8
+ # grade: submission.grade,
9
+ # old_grade: old_submission.try(:grade),
10
+ # score: submission.score,
11
+ # old_score: old_submission.try(:score),
12
+ # points_possible: submission.assignment.points_possible,
13
+ # old_points_possible: old_assignment.points_possible,
14
+ # grader_id: grader_id,
15
+ # student_id: submission.global_user_id,
16
+ # user_id: submission.global_user_id,
17
+ # grading_complete: submission.graded?,
18
+ # muted: submission.muted_assignment?
19
+ # }
20
+
21
+ def perform(event_payload)
22
+ super
23
+ attrs = {
24
+ submission_id: submission.try(:id),
25
+ assignment_id: assignment.try(:id),
26
+ score: payload[:score],
27
+ points_possible: payload[:points_possible],
28
+ grader_id: payload[:grader_id],
29
+ user_id: user.try(:id),
30
+ grading_complete: payload[:grading_complete],
31
+ muted: payload[:muted]
32
+ }
33
+ create_or_update(attrs)
34
+ rescue => e
35
+ log_error e
36
+ ensure
37
+ true
38
+ end
39
+
40
+ private
41
+ def create_or_update(attrs)
42
+ # Todo: add your code here
43
+ end
44
+
45
+ def submission
46
+ Submission.find_by(canvas_submission_id: local_canvas_id(payload[:submission_id]))
47
+ end
48
+
49
+ def assignment
50
+ Assignment.find_by(canvas_assignment_id: local_canvas_id(payload[:assignment_id]))
51
+ end
52
+
53
+ def user
54
+ User.find_by(canvas_user_id: local_canvas_id(payload[:user_id]))
55
+ end
56
+ end
@@ -0,0 +1,4 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::SubmissionCreatedEvent < LiveEvents::SubmissionEvent
4
+ end
@@ -0,0 +1,57 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::SubmissionEvent < LiveEvents::BaseEvent
4
+
5
+ # The following is provided in the live events call:
6
+ # {
7
+ # submission_id: submission.global_id,
8
+ # assignment_id: submission.global_assignment_id,
9
+ # user_id: submission.global_user_id,
10
+ # submitted_at: submission.submitted_at,
11
+ # graded_at: submission.graded_at,
12
+ # updated_at: submission.updated_at,
13
+ # score: submission.score,
14
+ # grade: submission.grade,
15
+ # submission_type: submission.submission_type,
16
+ # body: LiveEvents.truncate(submission.body),
17
+ # url: submission.url,
18
+ # attempt: submission.attempt
19
+ # }
20
+
21
+ def perform(event_payload)
22
+ super
23
+ return if assignment.nil? || user.nil?
24
+ attrs = {
25
+ canvas_submission_id: local_canvas_id(payload[:submission_id]),
26
+ assignment_id: assignment.try(:id),
27
+ user_id: user.try(:id),
28
+ submitted_at: payload[:submitted_at],
29
+ graded_at: payload[:graded_at],
30
+ score: payload[:score],
31
+ grade: payload[:grade],
32
+ submission_type: payload[:submission_type]
33
+ }
34
+ create_or_update(attrs)
35
+ rescue => e
36
+ log_error e
37
+ ensure
38
+ true
39
+ end
40
+ end
41
+
42
+ private
43
+ def create_or_update(attrs)
44
+ submission = Submission.where(canvas_submission_id: attrs[:canvas_submission_id]).first_or_initialize
45
+ submission.assign_attributes(attrs)
46
+ submission.save!
47
+ submission
48
+ end
49
+
50
+
51
+ def assignment
52
+ @assignment ||= Assignment.find_by(canvas_assignment_id: local_canvas_id(payload[:assignment_id]))
53
+ end
54
+
55
+ def user
56
+ @user ||= User.find_by(canvas_user_id: local_canvas_id(payload[:user_id]))
57
+ end
@@ -0,0 +1,4 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::SubmissionUpdatedEvent < LiveEvents::SubmissionEvent
4
+ end
@@ -0,0 +1,35 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::SyllabusEvent < LiveEvents::BaseEvent
4
+ # The following is provided in the live events call:
5
+ # {
6
+ # course_id: course.global_id,
7
+ # syllabus_body: LiveEvents.truncate(course.syllabus_body),
8
+ # old_syllabus_body: LiveEvents.truncate(old_syllabus_body)
9
+ # }
10
+ #
11
+
12
+ def perform(event_payload)
13
+ super
14
+ attrs = {
15
+ course_id: course.try(:id),
16
+ syllabus_body: payload[:syllabus_body]
17
+ }
18
+ create_or_update(attrs)
19
+ rescue => e
20
+ log_error e
21
+ ensure
22
+ true
23
+ end
24
+
25
+ private
26
+ def create_or_update(attrs)
27
+ syllabus = Syllabus.where(course_id: attrs[:course_id]).first_or_initialize
28
+ syllabus.body = attrs[:syllabus_body] # Syllabus Body can get truncated by live events
29
+ syllabus.save!
30
+ end
31
+
32
+ def course
33
+ Course.find_by(canvas_course_id: local_canvas_id(payload[:course_id]))
34
+ end
35
+ end
@@ -0,0 +1,2 @@
1
+ class LiveEvents::SyllabusUpdatedEvent < LiveEvents::SyllabusEvent
2
+ end
@@ -0,0 +1,4 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::UserChangedEvent < LiveEvents::UserEvent
4
+ end
@@ -0,0 +1,35 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::UserEvent < LiveEvents::BaseEvent
4
+ # The following is provided in the live events call:
5
+ # {
6
+ # user_id: user.global_id,
7
+ # uuid: user.uuid,
8
+ # name: user.name,
9
+ # short_name: user.short_name,
10
+ # workflow_state: user.workflow_state,
11
+ # created_at: user.created_at,
12
+ # updated_at: user.updated_at
13
+ # }
14
+
15
+ def perform(event_payload)
16
+ super
17
+ attrs = {
18
+ canvas_user_id: local_canvas_id(payload[:user_id]),
19
+ name: payload[:name],
20
+ workflow_state: payload[:workflow_state]
21
+ }
22
+ create_or_update(attrs)
23
+ rescue => e
24
+ log_error e
25
+ ensure
26
+ true
27
+ end
28
+
29
+ private
30
+ def create_or_update(attrs)
31
+ user = User.where(canvas_user_id: attrs[:canvas_user_id]).first_or_initialize
32
+ user.assign_attributes(attrs)
33
+ user.save!
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ <%= autogenerated_event_warning %>
2
+
3
+ class LiveEvents::UserUpdatedEvent < LiveEvents::UserEvent
4
+ end
@@ -31,7 +31,7 @@ module CanvasSync
31
31
  row_ids[row[conflict_target]] = true
32
32
 
33
33
  rows << csv_column_names.map do |column|
34
- if mapping[column][:type] == :datetime
34
+ if mapping[column][:type].to_sym == :datetime
35
35
  # todo - add some timezone config to the mapping.
36
36
  # In cases where the timestamp or date doesn't include a timezone, you should be able to specify one
37
37
  DateTime.parse(row[column]).utc rescue ''
@@ -0,0 +1,18 @@
1
+ module CanvasSync
2
+ module Jobs
3
+ class SyncRolesJob < CanvasSync::Job
4
+ # Syncs Roles using the Canvas API
5
+ #
6
+ #
7
+ # @param job_chain [Hash]
8
+ # @param options [Hash]
9
+ def perform(job_chain, options)
10
+ CanvasSync.get_canvas_sync_client(job_chain[:global_options]).list_roles('self').all_pages!.each do |role_params|
11
+ Role.create_or_update(role_params)
12
+ end
13
+
14
+ CanvasSync.invoke_next(job_chain)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module CanvasSync
2
- VERSION = '0.3.3'
2
+ VERSION = '0.3.5'
3
3
  end
@@ -50,6 +50,34 @@ RSpec.describe CanvasSync do
50
50
  })
51
51
  end
52
52
  end
53
+
54
+ context 'we are syncing roles with a term scope' do
55
+ it 'syncs the roles in a separate job that runs first' do
56
+ chain = CanvasSync.default_provisioning_report_chain(['roles', 'courses'], :active)
57
+ expect(chain).to eq({
58
+ jobs: [
59
+ { job: CanvasSync::Jobs::SyncTermsJob.to_s, options: {} },
60
+ { job: CanvasSync::Jobs::SyncRolesJob.to_s, options: {} },
61
+ { job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s, options: { term_scope: 'active', models: ['courses'] } }
62
+ ],
63
+ global_options: { legacy_support: false }
64
+ })
65
+ end
66
+ end
67
+
68
+ context 'we are syncing roles without a term scope' do
69
+ it 'syncs roles separately even with no term scope' do
70
+ chain = CanvasSync.default_provisioning_report_chain(['roles', 'courses'])
71
+ expect(chain).to eq({
72
+ jobs: [
73
+ { job: CanvasSync::Jobs::SyncTermsJob.to_s, options: {} },
74
+ { job: CanvasSync::Jobs::SyncRolesJob.to_s, options: {} },
75
+ { job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s, options: { term_scope: nil, models: ['courses'] } }
76
+ ],
77
+ global_options: { legacy_support: false }
78
+ })
79
+ end
80
+ end
53
81
  end
54
82
  end
55
83
  end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync::Jobs::SyncRolesJob do
4
+ describe '#perform' do
5
+ let(:role_params) { open_canvas_fixture('roles') }
6
+ let(:job_chain) { { jobs: [], global_options: {}} }
7
+
8
+ it 'retrieves all terms from the Canvas API and then invokes the next job' do
9
+ expect(CanvasSync).to receive(:invoke_next).with(job_chain)
10
+
11
+ expect {
12
+ CanvasSync::Jobs::SyncRolesJob.perform_now(job_chain, {})
13
+ }.to change { Role.count }.by(role_params.length)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Role, type: :model do
4
+ let(:subject) { FactoryGirl.create(:role) }
5
+
6
+ describe 'validations' do
7
+ it { should validate_presence_of(:canvas_role_id) }
8
+ it { should validate_uniqueness_of(:canvas_role_id) }
9
+ end
10
+
11
+ describe '.create_or_update' do
12
+ let(:role_params) { open_canvas_fixture('roles')[0] }
13
+
14
+ context 'the role does not already exist' do
15
+ it 'creates it' do
16
+ expect {
17
+ Role.create_or_update(role_params)
18
+ }.to change { Role.count }.by(1)
19
+
20
+ role = Role.last
21
+ expect(role.label).to eq(role_params['label'])
22
+ expect(role.base_role_type).to eq(role_params['base_role_type'])
23
+ expect(role.account).to eq(role_params['account'])
24
+ expect(role.permissions).to eq(role_params['permissions'])
25
+ expect(role.workflow_state).to eq(role_params['workflow_state'])
26
+ end
27
+ end
28
+
29
+ context 'the role already exists' do
30
+ let!(:existing_role) { FactoryGirl.create(:role, canvas_role_id: role_params['id']) }
31
+
32
+ it 'updates it' do
33
+ expect {
34
+ Role.create_or_update(role_params)
35
+ }.to_not change { Role.count }
36
+
37
+ existing_role.reload
38
+ expect(existing_role.label).to eq(role_params['label'])
39
+ expect(existing_role.base_role_type).to eq(role_params['base_role_type'])
40
+ expect(existing_role.account).to eq(role_params['account'])
41
+ expect(existing_role.permissions).to eq(role_params['permissions'])
42
+ expect(existing_role.workflow_state).to eq(role_params['workflow_state'])
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # AUTO GENERATED MODEL
3
+ # This model was auto generated by the CanvasSync Gem.
4
+ # You can customize it as needed, but make sure you test
5
+ # any changes you make to the auto generated methods.
6
+ #
7
+
8
+
9
+ class Role < ApplicationRecord
10
+ validates :canvas_role_id, uniqueness: true, presence: true
11
+
12
+ def self.create_or_update(role_params)
13
+ role = Role.find_or_initialize_by(canvas_role_id: role_params['id'])
14
+
15
+ role.assign_attributes(label: role_params['label'],
16
+ base_role_type: role_params['base_role_type'],
17
+ account: role_params['account'],
18
+ permissions: role_params['permissions'],
19
+ workflow_state: role_params['workflow_state'])
20
+
21
+ role.save! if role.changed?
22
+ role
23
+ end
24
+ end
@@ -5,21 +5,21 @@
5
5
  # gem 'sqlite3'
6
6
  #
7
7
  default: &default
8
- adapter: sqlite3
8
+ adapter: postgresql
9
9
  pool: 5
10
10
  timeout: 5000
11
11
 
12
12
  development:
13
13
  <<: *default
14
- database: db/development.sqlite3
14
+ database: canvas_sync_development
15
15
 
16
16
  # Warning: The database defined as "test" will be erased and
17
17
  # re-generated from your development database when you run "rake".
18
18
  # Do not set this db to the same as development or production.
19
19
  test:
20
20
  <<: *default
21
- database: db/test.sqlite3
21
+ database: canvas_sync_test
22
22
 
23
23
  production:
24
24
  <<: *default
25
- database: db/production.sqlite3
25
+ database: canvas_sync_production
@@ -0,0 +1,23 @@
1
+ #
2
+ # AUTO GENERATED MIGRATION
3
+ # This migration was auto generated by the CanvasSync Gem.
4
+ # You can add new columns to this table, but removing or
5
+ # re-naming ones created here may break Canvas Syncing.
6
+ #
7
+
8
+
9
+ class CreateRoles < ActiveRecord::Migration[5.1]
10
+ def change
11
+ create_table :roles do |t|
12
+ t.integer :canvas_role_id, null: false
13
+ t.string :label, null: false
14
+ t.string :base_role_type, null: false
15
+ t.json :account
16
+ t.string :workflow_state, null: false
17
+ t.json :permissions
18
+
19
+ t.timestamps
20
+ end
21
+ add_index :roles, :canvas_role_id, unique: true
22
+ end
23
+ end
@@ -10,7 +10,10 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 20171107213207) do
13
+ ActiveRecord::Schema.define(version: 20180103162102) do
14
+
15
+ # These are extensions that must be enabled in order to support this database
16
+ enable_extension "plpgsql"
14
17
 
15
18
  create_table "canvas_sync_job_logs", force: :cascade do |t|
16
19
  t.datetime "started_at"
@@ -60,6 +63,18 @@ ActiveRecord::Schema.define(version: 20171107213207) do
60
63
  t.index ["canvas_user_id"], name: "index_enrollments_on_canvas_user_id"
61
64
  end
62
65
 
66
+ create_table "roles", force: :cascade do |t|
67
+ t.integer "canvas_role_id", null: false
68
+ t.string "label", null: false
69
+ t.string "base_role_type", null: false
70
+ t.json "account"
71
+ t.string "workflow_state", null: false
72
+ t.json "permissions"
73
+ t.datetime "created_at", null: false
74
+ t.datetime "updated_at", null: false
75
+ t.index ["canvas_role_id"], name: "index_roles_on_canvas_role_id", unique: true
76
+ end
77
+
63
78
  create_table "sections", force: :cascade do |t|
64
79
  t.bigint "canvas_section_id", null: false
65
80
  t.string "sis_id"