canvas_shim 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +21 -0
  5. data/app/assets/config/canvas_shim_manifest.js +2 -0
  6. data/app/assets/javascripts/canvas_shim/application.js +13 -0
  7. data/app/assets/stylesheets/canvas_shim/application.css +15 -0
  8. data/app/controllers/canvas_shim/application_controller.rb +12 -0
  9. data/app/controllers/canvas_shim/settings_api/swagger_controller.rb +11 -0
  10. data/app/controllers/canvas_shim/settings_api/v1/users_controller.rb +27 -0
  11. data/app/deploy/canvas_shim_asset_uploader.rb +19 -0
  12. data/app/helpers/canvas_shim/application_helper.rb +4 -0
  13. data/app/jobs/canvas_shim/application_job.rb +4 -0
  14. data/app/mailers/canvas_shim/application_mailer.rb +6 -0
  15. data/app/models/canvas_shim/application_record.rb +5 -0
  16. data/app/services/courses_service.rb +5 -0
  17. data/app/services/courses_service/commands/distribute_due_dates.rb +50 -0
  18. data/app/services/courses_service/commands/distribute_due_dates/scheduler.rb +70 -0
  19. data/app/services/grades_service.rb +33 -0
  20. data/app/services/grades_service/account.rb +13 -0
  21. data/app/services/grades_service/commands/zero_out_assignment_grades.rb +73 -0
  22. data/app/services/grades_service/commands/zero_out_assignment_grades_rollback.rb +38 -0
  23. data/app/services/pipeline_service.rb +27 -0
  24. data/app/services/pipeline_service/account.rb +9 -0
  25. data/app/services/pipeline_service/api/publish.rb +53 -0
  26. data/app/services/pipeline_service/commands/publish.rb +44 -0
  27. data/app/services/pipeline_service/commands/publish_events.rb +17 -0
  28. data/app/services/pipeline_service/endpoints/pipeline.rb +62 -0
  29. data/app/services/pipeline_service/endpoints/pipeline/message_builder.rb +75 -0
  30. data/app/services/pipeline_service/events/emitter.rb +56 -0
  31. data/app/services/pipeline_service/events/graded_out_event.rb +31 -0
  32. data/app/services/pipeline_service/events/http_client.rb +14 -0
  33. data/app/services/pipeline_service/events/responders/sis.rb +66 -0
  34. data/app/services/pipeline_service/events/subscription.rb +11 -0
  35. data/app/services/pipeline_service/http_client.rb +21 -0
  36. data/app/services/pipeline_service/logger.rb +39 -0
  37. data/app/services/pipeline_service/pipeline_client.rb +41 -0
  38. data/app/services/pipeline_service/serializers/assignment.rb +55 -0
  39. data/app/services/pipeline_service/serializers/base_methods.rb +33 -0
  40. data/app/services/pipeline_service/serializers/canvas_api_enrollment.rb +55 -0
  41. data/app/services/pipeline_service/serializers/enrollment.rb +31 -0
  42. data/app/services/pipeline_service/serializers/fetcher.rb +17 -0
  43. data/app/services/pipeline_service/serializers/submission.rb +30 -0
  44. data/app/services/pipeline_service/serializers/user.rb +31 -0
  45. data/app/services/settings_service.rb +51 -0
  46. data/app/services/settings_service/api_docs.yml +68 -0
  47. data/app/services/settings_service/assignment.rb +24 -0
  48. data/app/services/settings_service/assignment_repository.rb +89 -0
  49. data/app/services/settings_service/auth_middleware.rb +19 -0
  50. data/app/services/settings_service/auth_token.rb +21 -0
  51. data/app/services/settings_service/authenticator_stub.rb +9 -0
  52. data/app/services/settings_service/commands/get_enrollment_settings.rb +17 -0
  53. data/app/services/settings_service/commands/get_settings.rb +34 -0
  54. data/app/services/settings_service/commands/get_user_settings.rb +18 -0
  55. data/app/services/settings_service/commands/update_enrollment_setting.rb +22 -0
  56. data/app/services/settings_service/commands/update_settings.rb +38 -0
  57. data/app/services/settings_service/commands/update_user_setting.rb +21 -0
  58. data/app/services/settings_service/enrollment.rb +28 -0
  59. data/app/services/settings_service/queries/zero_grader_audit.rb +6 -0
  60. data/app/services/settings_service/repository.rb +93 -0
  61. data/app/services/settings_service/school.rb +26 -0
  62. data/app/services/settings_service/student_assignment.rb +24 -0
  63. data/app/services/settings_service/student_assignment_repository.rb +116 -0
  64. data/app/services/settings_service/submission.rb +24 -0
  65. data/app/services/settings_service/user.rb +28 -0
  66. data/app/views/layouts/canvas_shim/application.html.erb +14 -0
  67. data/config/initializers/hash.rb +17 -0
  68. data/config/initializers/string.rb +9 -0
  69. data/config/routes.rb +9 -0
  70. data/lib/canvas_shim.rb +5 -0
  71. data/lib/canvas_shim/engine.rb +12 -0
  72. data/lib/canvas_shim/version.rb +3 -0
  73. data/lib/tasks/canvas_shim_tasks.rake +7 -0
  74. metadata +214 -0
@@ -0,0 +1,38 @@
1
+ module GradesService
2
+ module Commands
3
+ class ZeroOutAssignmentGradesRollback
4
+ def call!(options={})
5
+ load_audit(options)
6
+
7
+ CSV.foreach('/tmp/zero_grader_rollback.csv') do |row|
8
+ submission_id = row[0]
9
+ orig_score = row[1]
10
+ submission = Submission.find(submission_id)
11
+ user = submission.user
12
+ grader = GradesService::Account.account_admin
13
+ submission = Submission.find(submission_id)
14
+ assignment = submission.assignment
15
+
16
+ if orig_score.present? && submission.score == 0 || submission.score.nil?
17
+ puts "Setting submission #{submission.id} from #{submission.score} to #{orig_score or 'nil'}"
18
+ begin
19
+ puts "assignment.grade_student(#{user.id}, score: #{orig_score or 'nil'}, grader: 1)"
20
+ assignment.grade_student(user, score: orig_score, grader: grader)
21
+ rescue => e
22
+ puts "Failed Setting submission #{submission.id} from #{submission.score} to #{orig_score or 'nil'}: #{e}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def load_audit(options)
31
+ s3 = Aws::S3::Client.new(access_key_id: ENV['S3_ACCESS_KEY_ID'], secret_access_key: ENV['S3_ACCESS_KEY'])
32
+ File.open('/tmp/zero_grader_rollback.csv', 'w') do |file|
33
+ s3.get_object({ bucket: ENV['S3_BUCKET_NAME'], key: 'zero_grader/' + options[:log_file] }, target: file)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ # The public api for the PipelineService
2
+ # By default the command will be enqueued.
3
+ #
4
+ # Example: PipelineService.publish(User.first)
5
+ module PipelineService
6
+ cattr_reader :queue_mode
7
+ def self.publish(object, api: API::Publish, noun: nil)
8
+ return if SettingsService.get_settings(object: :school, id: 1)['disable_pipeline']
9
+ api.new(object, noun: noun).call
10
+ end
11
+
12
+ def self.perform_synchronously?
13
+ queue_mode == 'synchronous'
14
+ end
15
+
16
+ @@queue_mode = ENV['SYNCHRONOUS_PIPELINE_JOBS'] == 'true' ? 'synchronous' : 'asynchronous'
17
+ def self.queue_mode=(mode)
18
+ case mode
19
+ when 'synchronous'
20
+ @@queue_mode = 'synchronous'
21
+ when 'asynchronous'
22
+ @@queue_mode = 'asynchronous'
23
+ else
24
+ raise 'unknown queue mode: ' + mode
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ module PipelineService
2
+ module Account
3
+ def self.account_admin
4
+ account = ::Account.default.account_users.find do |account_user|
5
+ account_user.role.name == 'AccountAdmin'
6
+ end.try(:user)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ # The API calls Commands
2
+ #
3
+ # Class methods map to Commands
4
+ # ie: PipelineService::API::Publish calls PipelineService::Commands::Publish
5
+ #
6
+ module PipelineService
7
+ module API
8
+ class Publish
9
+ def initialize(object, args={})
10
+ @object = object
11
+ @changes = object.try(:changes)
12
+ @noun = args[:noun]
13
+ @args = args
14
+ configure_dependencies
15
+ end
16
+
17
+ def call
18
+ return if SettingsService.get_settings(object: :school, id: 1)['disable_pipeline']
19
+ queue.enqueue(self, priority: 1000000)
20
+ end
21
+
22
+ def perform
23
+ command.call
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :object, :jobs, :command_class, :queue, :noun, :changes
29
+
30
+ def configure_dependencies
31
+ @command_class = @args[:command_class] || Commands::Publish
32
+ @queue = @args[:queue] || Delayed::Job
33
+ end
34
+
35
+
36
+ def subscriptions
37
+ Events::Subscription.new(
38
+ event: 'graded_out',
39
+ responder: Events::Responders::SIS
40
+ )
41
+ end
42
+
43
+ def command
44
+ command_class.new(
45
+ object: object,
46
+ event_subscriptions: subscriptions,
47
+ noun: noun,
48
+ changes: changes
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,44 @@
1
+ module PipelineService
2
+ module Commands
3
+ # Serialize a canvas object into their API format and
4
+ # send to the pipeline
5
+ #
6
+ # Usage:
7
+ # Send.new(message: User.last).call
8
+ class Publish
9
+ attr_reader :message, :serializer
10
+
11
+ def initialize(args)
12
+ @args = args
13
+ @object = args[:object]
14
+ @changes = args[:changes]
15
+ configure_dependencies
16
+ end
17
+
18
+ def call
19
+ return if SettingsService.get_settings(object: :school, id: 1)['disable_pipeline']
20
+ post_to_pipeline
21
+ publish_events unless changes.nil?
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :object, :client, :responder, :changes
28
+
29
+ def configure_dependencies
30
+ @client = @args[:client] || PipelineClient
31
+ end
32
+
33
+ def publish_events
34
+ Commands::PublishEvents.new(@args).call
35
+ end
36
+
37
+ def post_to_pipeline
38
+ client.new(
39
+ @args.merge(object: object)
40
+ ).call
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ module PipelineService
2
+ module Commands
3
+ class PublishEvents
4
+ def initialize(args={})
5
+ @args = args
6
+ end
7
+
8
+ def call
9
+ self.class.emitter.new(@args).call
10
+ end
11
+
12
+ def self.emitter
13
+ Events::Emitter
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,62 @@
1
+ module PipelineService
2
+ module Endpoints
3
+ class Pipeline
4
+ def initialize(args={})
5
+ @args = args
6
+ configure_dependencies
7
+ raise 'Missing config' if missing_config?
8
+ end
9
+
10
+ def call
11
+ return if SettingsService.get_settings(object: :school, id: 1)['disable_pipeline']
12
+ if PipelineService.perform_synchronously?
13
+ perform
14
+ else
15
+ Delayed::Job.enqueue(self, priority: 1000000)
16
+ end
17
+ self
18
+ end
19
+
20
+ def perform
21
+ configure_publisher
22
+ build_payload
23
+ post
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :endpoint, :username, :password, :publisher, :message_builder, :payload
29
+
30
+ def build_payload
31
+ @payload = message_builder.new(@args).call
32
+ end
33
+
34
+ def configure_dependencies
35
+ @message_builder = @args[:message_builder] || MessageBuilder
36
+ @publisher = @args[:publisher] || PipelinePublisher
37
+ @endpoint = ENV['PIPELINE_ENDPOINT']
38
+ @username = ENV['PIPELINE_USER_NAME']
39
+ @password = ENV['PIPELINE_PASSWORD']
40
+ end
41
+
42
+ def missing_config?
43
+ [endpoint, username, password].any?(&:nil?)
44
+ end
45
+
46
+ def configure_publisher
47
+ publisher.configure do |config|
48
+ config.host = endpoint
49
+ config.username = username
50
+ config.password = password
51
+ if endpoint.slice(0,7) == 'http://'
52
+ config.scheme = 'http'
53
+ end
54
+ end
55
+ end
56
+
57
+ def post
58
+ PipelineService::HTTPClient.post(payload)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ module PipelineService
2
+ module Endpoints
3
+ class Pipeline
4
+ class MessageBuilder
5
+ SOURCE = 'canvas'
6
+
7
+ def initialize(args)
8
+ @object = args[:object]
9
+ @serializer = args[:serializer]
10
+ @args = args
11
+ configure_dependencies
12
+ end
13
+
14
+ def call
15
+ fetch_serializer
16
+ serialize
17
+ result = build
18
+ log
19
+ result
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :message_class, :object, :fetcher, :serializer, :canvas_domain, :logger, :serialized_object
25
+
26
+ def serialize
27
+ @serialized_object = serializer.new(object: object).call
28
+ end
29
+
30
+ def log
31
+ Logger.new(source: 'pipeline', message: payload).call
32
+ end
33
+
34
+ def payload
35
+ {
36
+ noun: noun,
37
+ meta: {
38
+ source: SOURCE,
39
+ domain_name: canvas_domain,
40
+ api_version: 1,
41
+ status: object.try(:state)
42
+ },
43
+ identifiers: { id: id },
44
+ data: serialized_object
45
+ }
46
+ end
47
+
48
+ def configure_dependencies
49
+ @message_class = @args[:message_class] || PipelinePublisher::Message
50
+ @fetcher = @args[:fetcher] || Serializers::Fetcher
51
+ @logger = @args[:logger] || PipelineService::Logger
52
+ @canvas_domain = ENV['CANVAS_DOMAIN']
53
+ end
54
+
55
+ def fetch_serializer
56
+ return if @serializer
57
+ @serializer = fetcher.fetch(object: object)
58
+ end
59
+
60
+ def build
61
+ message_class.new(payload.to_hash)
62
+ end
63
+
64
+ def noun
65
+ object.class.to_s.underscore
66
+ end
67
+
68
+ def id
69
+ return object.id unless object.is_a?(Hash)
70
+ object[:id]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,56 @@
1
+ module PipelineService
2
+ module Events
3
+ class Emitter
4
+ def initialize(args={})
5
+ @object = args[:object]
6
+ @args = args
7
+ @responder = @args[:responder] || Events::Responders::SIS
8
+ @event = Events::GradedOutEvent
9
+ end
10
+
11
+ def call
12
+ return unless object.is_a?(Enrollment)
13
+ fetch_serializer
14
+ build_message
15
+ build_responder
16
+ build_subscriptions
17
+ emit
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :subscriptions, :object, :responder, :message, :serializer, :event
23
+
24
+ def build_subscriptions
25
+ @subscriptions = [
26
+ Events::Subscription.new(
27
+ event: :graded_out,
28
+ responder: responder
29
+ )
30
+ ]
31
+ end
32
+
33
+ def build_responder
34
+ @responder = responder.new(
35
+ object: object,
36
+ message: message
37
+ )
38
+ end
39
+
40
+ def fetch_serializer
41
+ @serializer = Serializers::CanvasAPIEnrollment
42
+ end
43
+
44
+ def build_message
45
+ @message = serializer.new(object: object).call
46
+ end
47
+
48
+ def emit
49
+ subscriptions.each do |subscription|
50
+ next if subscription.event != :graded_out
51
+ event.new(@args.merge(subscription: subscription)).emit
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,31 @@
1
+ module PipelineService
2
+ module Events
3
+ class GradedOutEvent
4
+ def initialize(args)
5
+ @args = args
6
+ @subscription = args[:subscription]
7
+ @object = args[:object]
8
+ @changes = args[:changes]
9
+ end
10
+
11
+ def emit
12
+ return unless should_trigger?
13
+ subscription.responder.call
14
+ end
15
+
16
+ private
17
+
18
+ attr_accessor :subscription, :object, :changes
19
+
20
+ def should_trigger?
21
+ return unless object.is_a?(::Enrollment)
22
+ recently_completed?
23
+ end
24
+
25
+ def recently_completed?
26
+ return false unless changes['workflow_state']
27
+ changes['workflow_state'][1] == 'completed'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module PipelineService
2
+ module Events
3
+ class HTTPClient
4
+ include Singleton
5
+ def self.post(endpoint, args={})
6
+ instance.post(endpoint, args)
7
+ end
8
+
9
+ def post(endpoint, args)
10
+ HTTParty.post(endpoint, args)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,66 @@
1
+ module PipelineService
2
+ module Events
3
+ module Responders
4
+ class SIS
5
+ HEADERS = { 'Content-Type' => 'application/json' }
6
+
7
+ def initialize(object:, message:, args: {})
8
+ @message = message
9
+ @args = args
10
+ configure_dependencies
11
+ end
12
+
13
+ def call
14
+ raise 'Missing config' if missing_config?
15
+
16
+ if PipelineService.perform_synchronously?
17
+ perform
18
+ else
19
+ queue.enqueue(self)
20
+ end
21
+ end
22
+
23
+ def perform
24
+ post
25
+ log
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :message, :api_key, :endpoint, :args, :queue
31
+
32
+ def configure_dependencies
33
+ @api_key = ENV['SIS_ENROLLMENT_UPDATE_API_KEY']
34
+ @endpoint = ENV['SIS_ENROLLMENT_UPDATE_ENDPOINT']
35
+ @queue = args[:queue] || Delayed::Job
36
+ end
37
+
38
+ def missing_config?
39
+ [@api_key, @endpoint].any?(&:nil?)
40
+ end
41
+
42
+ def log
43
+ PipelineService::Logger.new(
44
+ source: 'pipeline_event::graded_out',
45
+ message: message,
46
+ enpdoint: build_endpoint
47
+ ).call
48
+ end
49
+
50
+ def build_endpoint
51
+ endpoint + '?apiKey=' + api_key
52
+ end
53
+
54
+ def post
55
+ return unless message
56
+
57
+ PipelineService::Events::HTTPClient.post(
58
+ build_endpoint,
59
+ body: message.to_json,
60
+ headers: HEADERS
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end