canvas_shim 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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