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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b606c1272841be003a1dfe4aa88c27cae616b9ef
4
+ data.tar.gz: d62427955584f06d749baa4e4993213c965f51a5
5
+ SHA512:
6
+ metadata.gz: ec4dd1a8f2e4c13bcbc208861f1b6f9e91373687fda73a37099a1fa4b21e287d6508fd5e164392af6c7df1ba1544a2e63c4682ae3465f8000593dae03c0d6182
7
+ data.tar.gz: 82fdab6766369d97b297560143d348a639762be5fd53d5375c13a70f93a1d9cde64037d4f8892c9617bc10ba3381ebd0fb83a30ceb301f7802dd4ff7f39fbc37
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # CanvasShim
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'canvas_shim'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install canvas_shim
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'CanvasShim'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/test_app/Rakefile", __FILE__)
18
+ task :default => ['app:spec:services']
19
+ load 'rails/tasks/engine.rake'
20
+ load 'rails/tasks/statistics.rake'
21
+ require 'bundler/gem_tasks'
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/canvas_shim .js
2
+ //= link_directory ../stylesheets/canvas_shim .css
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,12 @@
1
+ module CanvasShim
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ def validate_key
6
+ token = SettingsService::AuthToken.authenticate(
7
+ request.headers['HTTP_AUTHORIZATION'].try(:gsub, 'Bearer ', '')
8
+ )
9
+ render(json: {status: 'error'}, status: 401) and return unless token
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module CanvasShim
2
+ module SettingsApi
3
+ class SwaggerController < ApplicationController
4
+ def show
5
+ render json: YAML.load_file(
6
+ Engine.root.to_s + "/app/services/settings_service/api_docs.yml"
7
+ ).to_json
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ module CanvasShim
2
+ module SettingsApi
3
+ module V1
4
+ class UsersController < CanvasShim::ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+ before_action :validate_key
7
+
8
+ def update
9
+ settings.each do |key, value|
10
+ SettingsService.update_user_setting(
11
+ id: params[:id],
12
+ setting: key,
13
+ value: value
14
+ )
15
+ end
16
+ render json: { status: 'ok' }
17
+ end
18
+
19
+ private
20
+
21
+ def settings
22
+ params[:user]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ require 'aws-sdk-s3'
2
+ class CanvasShimAssetUploader < Canvas::Cdn::S3Uploader
3
+ BUCKET_NAME = ENV['ASSET_OBJECT_STORE'] || ''
4
+ REGION = ENV['AWS_REGION'] || 'us-west-2'
5
+ ACCESS_KEY_ID = ENV['S3_ACCESS_KEY_ID'] || ''
6
+ SECRET_ACCESS_KEY = ENV['S3_ACCESS_KEY'] || ''
7
+
8
+ def initialize(folder='dist')
9
+ require 'aws-sdk-s3'
10
+ @folder = folder
11
+ @s3 = Aws::S3::Resource.new(
12
+ access_key_id: ACCESS_KEY_ID,
13
+ secret_access_key: SECRET_ACCESS_KEY,
14
+ region: REGION
15
+ )
16
+ @bucket = @s3.bucket(BUCKET_NAME)
17
+ @mutex = Mutex.new
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ module CanvasShim
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module CanvasShim
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module CanvasShim
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module CanvasShim
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module CoursesService
2
+ def self.distribute_due_dates(course:)
3
+ Commands::DistributeDueDates.new(course: course).call
4
+ end
5
+ end
@@ -0,0 +1,50 @@
1
+ module CoursesService
2
+ module Commands
3
+ class DistributeDueDates
4
+ def initialize(args)
5
+ @args = args
6
+ @course = args[:course]
7
+ @modules = ContextModule
8
+ .where(
9
+ 'context_modules.context_id = ? AND context_modules.context_type = \'Course\' AND context_modules.name IS NOT NULL',
10
+ @course.id)
11
+ .order(:position)
12
+ end
13
+
14
+ def call
15
+ return unless SettingsService.get_settings(object: :school, id: 1)['auto_due_dates'] == 'on'
16
+ return unless course.start_at && course.end_at
17
+ course_assignments = assignments
18
+ scheduler.course_dates.each do |date, count|
19
+ update_assignments(course_assignments.slice!(0..count - 1), date)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :course, :assignments_per_day
26
+
27
+ def scheduler
28
+ @scheduler ||= Scheduler.new(@args.merge(assignment_count: assignments.count))
29
+ end
30
+
31
+ def update_assignments(assignments_for_day, date)
32
+ assignments_for_day.each do |assignment|
33
+ next if assignment.nil?
34
+ assignment.update(due_at: date)
35
+ end
36
+ end
37
+
38
+ def assignments
39
+ assignment_list = []
40
+ @modules.each do |context_module|
41
+ context_module.content_tags
42
+ .where(:content_type => ['Assignment', 'DiscussionTopic'])
43
+ .order(:position)
44
+ .map { |tag| assignment_list.push(tag.assignment) }
45
+ end
46
+ assignment_list
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ module CoursesService
2
+ module Commands
3
+ class DistributeDueDates
4
+ class Scheduler
5
+ WORKING_DAYS = %w( mon tue wed thu fri )
6
+
7
+ def initialize(args = {})
8
+ @args = args
9
+ @startdate = first_due_date
10
+ @enddate = args[:course].end_at
11
+ @assignment_count = args[:assignment_count]
12
+ end
13
+
14
+ def assignments_per_day
15
+ (assignment_count / business_days_count)
16
+ end
17
+
18
+ def course_dates
19
+ get_days
20
+ populate_assignment_counts
21
+ end
22
+
23
+ def business_days_count
24
+ calendar.business_days_between(startdate, enddate)
25
+ end
26
+
27
+ def course_days_count
28
+ enddate.to_date.mjd - startdate.to_date.mjd
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :assignment_count, :startdate, :enddate, :days
34
+
35
+ def first_due_date
36
+ @args[:course].start_at.in_time_zone(@args[:course].time_zone).at_end_of_day + 1.day
37
+ end
38
+
39
+ def calendar
40
+ if ENV['HOLIDAYS']
41
+ Business::Calendar.new(working_days: WORKING_DAYS, holidays: ENV['HOLIDAYS'].split(','))
42
+ else
43
+ Business::Calendar.new(working_days: WORKING_DAYS)
44
+ end
45
+
46
+ end
47
+
48
+ def get_days
49
+ @days ||= course_days_count.times.map do |count|
50
+ day = startdate + (count).days
51
+ day if calendar.business_day?(day)
52
+ end.compact
53
+ end
54
+
55
+ def populate_assignment_counts
56
+ result = {}
57
+ days.each do |day|
58
+ result[day] = assignments_per_day
59
+ end
60
+
61
+ (assignment_count % business_days_count).times.each do |num|
62
+ result[days[num]] = result[days[num]] + 1
63
+ end
64
+
65
+ result
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,33 @@
1
+ require 'fileutils'
2
+
3
+ module GradesService
4
+ def self.zero_out_grades!(options={})
5
+ return unless SettingsService.get_settings(object: :school, id: 1)['zero_out_past_due'] == 'on'
6
+
7
+ options[:log_file] = 'zero_grader_audit_' + Time.now.strftime('%Y%m%d%H%M') + '.csv'
8
+ FileUtils.touch('/tmp/' + options[:log_file])
9
+
10
+ submissions.find_each do |submission|
11
+ Commands::ZeroOutAssignmentGrades.new(submission).call!(options)
12
+ end
13
+ save_audit(options)
14
+ end
15
+
16
+ def self.submissions
17
+ Submission
18
+ .joins(assignment: :course)
19
+ .where('submissions.workflow_state = ?', 'unsubmitted')
20
+ .where(score: nil)
21
+ .where(grade: nil)
22
+ .where('assignments.due_at < ?', 1.hour.ago)
23
+ .where('courses.conclude_at > ?', Time.now)
24
+ .where('assignments.workflow_state = ?', 'published')
25
+ end
26
+
27
+ def self.save_audit(options)
28
+ return if options[:dry_run]
29
+ s3 = Aws::S3::Resource.new(region: ENV['AWS_REGION'], access_key_id: ENV['S3_ACCESS_KEY_ID'], secret_access_key: ENV['S3_ACCESS_KEY'])
30
+ obj = s3.bucket(ENV['S3_BUCKET_NAME']).object('zero_grader/' + options[:log_file])
31
+ obj.upload_file('/tmp/' + options[:log_file])
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ module GradesService
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
+
9
+ def self.strongmind_grader
10
+ User.new(id: -1001)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,73 @@
1
+ module GradesService
2
+ module Commands
3
+ class ZeroOutAssignmentGrades
4
+ def initialize(submission)
5
+ @previous_score = submission.score
6
+ @submission = submission
7
+ @assignment = @submission.assignment
8
+ @student = @submission.user
9
+ @course = @assignment.context
10
+ @grader = GradesService::Account.account_admin
11
+ end
12
+
13
+ def call!(options={})
14
+ return unless options[:log_file]
15
+ return unless SettingsService.get_settings(object: :school, id: 1)['zero_out_past_due'] == 'on'
16
+ @options = options
17
+ return unless should_grade?
18
+
19
+ if options[:dry_run]
20
+ log_execution_plan
21
+ return
22
+ end
23
+
24
+ log_operation
25
+ @assignment.grade_student(@student, score: 0, grader: @grader)
26
+ end
27
+
28
+ private
29
+
30
+ def log_execution_plan
31
+ file = File.open('dry_run.log', 'a+')
32
+ file.write("Changing submission #{@submission.id} from #{@previous_score || 'nil'} to 0\n")
33
+ file.close
34
+ end
35
+
36
+ def log_operation
37
+ return unless @options[:log_file]
38
+
39
+ begin
40
+ csv = CSV.open('/tmp/' + @options[:log_file], 'a+')
41
+ csv << [@submission.id, @previous_score]
42
+ csv.close
43
+ rescue => e
44
+ raise e, 'error opening log file'
45
+ end
46
+ end
47
+
48
+ def late?
49
+ return @assignment.due_at.present? && 1.hour.ago > @assignment.due_at
50
+ end
51
+
52
+ def unscored?
53
+ @submission.score.nil? && @submission.grade.nil?
54
+ end
55
+
56
+ def unsubmitted?
57
+ @submission.workflow_state == 'unsubmitted'
58
+ end
59
+
60
+ def published?
61
+ @assignment.published?
62
+ end
63
+
64
+ def enrolled?
65
+ @course.includes_user?(@student, @course.admin_visible_student_enrollments)
66
+ end
67
+
68
+ def should_grade?
69
+ unsubmitted? && unscored? && late? && published? && enrolled?
70
+ end
71
+ end
72
+ end
73
+ end