oneroster 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Bobby Julius
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,110 @@
1
+ # OneRoster
2
+
3
+ This is a simple Ruby API wrapper for OneRoster.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'one_roster'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install one_roster
20
+
21
+ ## Usage
22
+
23
+ ### Configuration
24
+ The gem can be initialized as follows:
25
+
26
+ ```ruby
27
+ client = OneRoster::Client.configure do |config|
28
+ config.app_id = 'app_id for district'
29
+ config.app_secret = 'app_secret for district'
30
+ config.api_url = 'api_url for district'
31
+ end
32
+ ```
33
+
34
+ ### Requests
35
+ This gem supports requesting:
36
+ - Students
37
+ - Teachers
38
+ - Classes
39
+ - Classrooms
40
+ - Courses
41
+ - Enrollments
42
+
43
+ #### Students
44
+ - Request all students:
45
+ ```ruby
46
+ client.students
47
+ ```
48
+ - Request a subset of students filtered by their `sourcedId`:
49
+ ```ruby
50
+ client.students([student_1['sourcedId']], student_2['sourcedId'], …])
51
+ ```
52
+ #### Teachers
53
+ - Request all students:
54
+ ```ruby
55
+ client.teachers
56
+ ```
57
+ - Request a subset of teachers filtered by their `sourcedId`:
58
+ ```ruby
59
+ client.teachers([teacher_1['sourcedId']], teacher_2['sourcedId'], …])
60
+ ```
61
+ #### Classes
62
+ - Request all classes:
63
+ ```ruby
64
+ client.classes
65
+ ```
66
+ - Request a subset of classes filtered by their `sourcedId`:
67
+ ```ruby
68
+ client.classes([class_1['sourcedId']], class_2['sourcedId'], …])
69
+ ```
70
+ #### Classrooms
71
+ - Request all classrooms:
72
+ ```ruby
73
+ client.classrooms
74
+ ```
75
+ - Request a subset of active classrooms whose UIDs are found in OneRoster's classes, filtered by their course's `sourcedId`:
76
+ ```ruby
77
+ client.classes([enrollment_1['course']['sourcedId'], enrollment_2['course']['sourcedId'], …])
78
+ ```
79
+ #### Courses
80
+ - Request all courses:
81
+ ```ruby
82
+ client.courses
83
+ ```
84
+ - Request a subset of active courses whose UIDs are found in OneRoster's classes, filtered by their `sourcedId`:
85
+ ```ruby
86
+ client.classes([course_1['sourcedId'], course_2['sourcedId'], …])
87
+ ```
88
+ #### Enrollments
89
+ - Request all enrollments
90
+ ```ruby
91
+ client.enrollments
92
+ ```
93
+ - Request a subset of active enrollments filtered by their class's `sourcedID`:
94
+ ```ruby
95
+ client.enrollments([class_1['sourcedId'], class_2['sourcedId']])
96
+ ```
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tci/oneroster.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'one_roster'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require 'pry'
11
+ Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry'
4
+
5
+ require 'dry/inflector'
6
+ require 'faraday'
7
+ require 'faraday_middleware'
8
+ require 'oauth'
9
+ require 'simple_oauth'
10
+
11
+ require 'one_roster/client'
12
+ require 'one_roster/connection'
13
+ require 'one_roster/paginator'
14
+ require 'one_roster/response'
15
+ require 'one_roster/version'
16
+
17
+ require 'types/base'
18
+ require 'types/course'
19
+ require 'types/class'
20
+ require 'types/classroom'
21
+ require 'types/enrollment'
22
+ require 'types/student'
23
+ require 'types/teacher'
24
+
25
+ module OneRoster
26
+ class DistrictNotFound < StandardError; end
27
+ class ConnectionError < StandardError; end
28
+
29
+ STUDENTS_ENDPOINT = 'ims/oneroster/v1p1/students'
30
+ TEACHERS_ENDPOINT = 'ims/oneroster/v1p1/teachers'
31
+ COURSES_ENDPOINT = 'ims/oneroster/v1p1/courses'
32
+ CLASSES_ENDPOINT = 'ims/oneroster/v1p1/classes'
33
+ ENROLLMENTS_ENDPOINT = 'ims/oneroster/v1p1/enrollments'
34
+
35
+ RESPONSE_TYPE_MAP = {
36
+ 'teachers' => 'users',
37
+ 'students' => 'users',
38
+ 'courses' => 'courses',
39
+ 'classes' => 'classes',
40
+ 'enrollments' => 'enrollments'
41
+ }.freeze
42
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OneRoster
4
+ class Client
5
+ attr_accessor :app_id, :app_token, :api_url,
6
+ :app_secret, :logger, :vendor_key,
7
+ :username_source, :oauth_strategy
8
+
9
+ attr_reader :authenticated
10
+
11
+ def initialize(oauth_strategy = 'oauth')
12
+ @authenticated = false
13
+ @oauth_strategy = oauth_strategy
14
+ end
15
+
16
+ def self.configure
17
+ client = new
18
+ yield(client) if block_given?
19
+ client
20
+ end
21
+
22
+ %i(students teachers classes).each do |record_type|
23
+ define_method(record_type) do |record_uids = []|
24
+ authenticate
25
+
26
+ endpoint = OneRoster.const_get("#{record_type.upcase}_ENDPOINT")
27
+
28
+ type = Types.const_get(Dry::Inflector.new.singularize(record_type.to_s.capitalize))
29
+
30
+ records = Paginator.fetch(connection, endpoint, :get, type, client: self).force
31
+
32
+ return records if record_uids.empty?
33
+
34
+ records.select { |record| record_uids.to_set.include?(record.uid) }
35
+ end
36
+ end
37
+
38
+ def classrooms(course_codes = [])
39
+ authenticate
40
+
41
+ oneroster_classes = classes
42
+
43
+ courses = courses(course_codes, oneroster_classes)
44
+
45
+ oneroster_classes.each_with_object([]) do |oneroster_class, oneroster_classes|
46
+ course = courses.find { |course| course.uid == oneroster_class.course_uid }
47
+ next unless course
48
+
49
+ oneroster_classes << Types::Classroom.new(
50
+ 'id' => oneroster_class.uid,
51
+ 'name' => oneroster_class.title,
52
+ 'course_number' => course.course_code,
53
+ 'period' => oneroster_class.period,
54
+ 'grades' => oneroster_class.grades
55
+ )
56
+ end
57
+ end
58
+
59
+ def courses(course_codes = [], oneroster_classes = classes)
60
+ authenticate
61
+
62
+ class_course_numbers = oneroster_classes.map(&:course_uid)
63
+
64
+ courses = Paginator.fetch(
65
+ connection,
66
+ COURSES_ENDPOINT,
67
+ :get,
68
+ Types::Course,
69
+ client: self
70
+ ).force
71
+
72
+ parse_courses(courses, course_codes, class_course_numbers)
73
+ end
74
+
75
+ def enrollments(classroom_uids = [])
76
+ authenticate
77
+
78
+ enrollments = parse_enrollments(classroom_uids)
79
+
80
+ p "Found #{enrollments.values.flatten.length} enrollments."
81
+
82
+ enrollments
83
+ end
84
+
85
+ def authenticate
86
+ return if authenticated?
87
+
88
+ if oauth_strategy == 'oauth2'
89
+ response = token
90
+
91
+ fail ConnectionError, response.raw_body unless response.success?
92
+
93
+ set_auth_headers(response.raw_body, response.headers['set-cookie'])
94
+ else
95
+ response = connection.execute(TEACHERS_ENDPOINT, :get, limit: 1)
96
+
97
+ fail ConnectionError, response.raw_body unless response.success?
98
+ end
99
+
100
+ @authenticated = true
101
+ end
102
+
103
+ def authenticated?
104
+ @authenticated
105
+ end
106
+
107
+ def connection
108
+ @connection ||= Connection.new(self, oauth_strategy)
109
+ end
110
+
111
+ def token
112
+ connection.execute("#{api_url}/token", :post)
113
+ end
114
+
115
+ def set_auth_headers(token, cookie)
116
+ connection.set_auth_headers(token['access_token'], cookie)
117
+ end
118
+
119
+ private
120
+
121
+ def parse_enrollments(classroom_uids = [])
122
+ enrollments = Paginator.fetch(
123
+ connection,
124
+ ENROLLMENTS_ENDPOINT,
125
+ :get,
126
+ Types::Enrollment,
127
+ client: self
128
+ ).force
129
+
130
+ enrollments.each_with_object(teacher: [], student: []) do |enrollment, enrollments|
131
+ next if classroom_uids.any? && !classroom_uids.include?(enrollment.classroom_uid)
132
+
133
+ enrollments[enrollment.role.to_sym] << enrollment if enrollment.valid?
134
+ end
135
+ end
136
+
137
+ def parse_courses(courses, course_codes, course_numbers)
138
+ courses.select do |course|
139
+ in_course_numbers = course_numbers.include?(course.uid)
140
+
141
+ if course_codes.any?
142
+ course_codes.include?(course.course_code) && in_course_numbers
143
+ else
144
+ in_course_numbers
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OneRoster
4
+ class Connection
5
+ OPEN_TIMEOUT = 60
6
+ TIMEOUT = 120
7
+
8
+ def initialize(client, oauth_strategy = 'oauth')
9
+ @client = client
10
+ @oauth_strategy = oauth_strategy
11
+ end
12
+
13
+ def execute(path, method = :get, params = nil, body = nil)
14
+ Response.new(raw_request(path, method, params, body))
15
+ end
16
+
17
+ def set_auth_headers(token, cookie)
18
+ connection.authorization :Bearer, token
19
+ @cookie = cookie
20
+ end
21
+
22
+ def connection
23
+ return @connection if @connection
24
+
25
+ @connection = if @oauth_strategy == 'oauth2'
26
+ oauth2_connection
27
+ else
28
+ oauth_connection
29
+ end
30
+ end
31
+
32
+ def log(message = '')
33
+ return unless @client.logger
34
+
35
+ @client.logger.info message
36
+ end
37
+
38
+ private
39
+
40
+ def raw_request(path, method, params, body)
41
+ p "request #{path} #{params}"
42
+
43
+ connection.public_send(method) do |request|
44
+ request.options.open_timeout = OPEN_TIMEOUT
45
+ request.options.timeout = TIMEOUT
46
+ request.url path, params
47
+ request.headers['Accept-Header'] = 'application/json'
48
+ request.headers['Cookie'] = @cookie
49
+ request.body = body
50
+ end
51
+ end
52
+
53
+ def oauth_connection
54
+ Faraday.new(@client.api_url) do |connection|
55
+ connection.request :oauth,
56
+ consumer_key: @client.app_id,
57
+ consumer_secret: @client.app_secret
58
+ connection.response :logger, @client.logger if @client.logger
59
+ connection.response :json, content_type: /\bjson$/
60
+ connection.adapter Faraday.default_adapter
61
+ end
62
+ end
63
+
64
+ def oauth2_connection
65
+ connection = Faraday.new(@client.api_url) do |connection|
66
+ connection.request :json
67
+ connection.response :logger, @client.logger if @client.logger
68
+ connection.response :json, content_type: /\bjson$/
69
+ connection.adapter Faraday.default_adapter
70
+ end
71
+ connection.basic_auth(@client.app_id, @client.app_secret)
72
+ connection
73
+ end
74
+ end
75
+ end