clever 1.2.5

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 bjulius
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,105 @@
1
+ # Clever
2
+
3
+ This is a simple Ruby API wrapper for Clever.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'clever'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install clever
20
+
21
+ ## Usage
22
+
23
+ ### Configuration
24
+ The gem can be initialized as follows:
25
+
26
+ ```ruby
27
+ client = Clever::Client.configure do |config|
28
+ config.app_id = 'app_id for district'
29
+ config.vendor_key = 'vendor_key for tci'
30
+ config.vendor_secret = 'vendor_secret for tci'
31
+ end
32
+ ```
33
+ ### Requests
34
+ This gem support requesting:
35
+ - Students
36
+ - Teachers
37
+ - Courses
38
+ - Sections
39
+ - Classrooms
40
+ - Enrollments
41
+
42
+ #### Students
43
+ - Request all students:
44
+ ```ruby
45
+ client.students
46
+ ```
47
+ - Request a subset of students, filtered by their `id`:
48
+ ```ruby
49
+ client.students([student_1['data']['id']], student_2['data']['id'], …])
50
+ ```
51
+ #### Teachers
52
+ - Request all teachers:
53
+ ```ruby
54
+ client.teachers
55
+ ```
56
+ - Request a subset of teachers, filtered by their `id`:
57
+ ```ruby
58
+ client.teachers([teacher_1['data']['id']], teacher_2['data']['id'], …])
59
+ ```
60
+ #### Courses
61
+ - Request all courses:
62
+ ```ruby
63
+ client.teachers
64
+ ```
65
+ - Request a subset of courses, filtered by their `id`:
66
+ ```ruby
67
+ client.courses([course_1['data']['id']], course_2['data']['id'], …])
68
+ ```
69
+ #### Sections
70
+ - Request all sections:
71
+ ```ruby
72
+ client.sections
73
+ ```
74
+ - Request a subset of sections, filtered by their `id`:
75
+ ```ruby
76
+ client.sections([section_1['data']['id']], section_2['data']['id'], …])
77
+ ```
78
+ #### Classrooms
79
+ - Request all classrooms
80
+ ```ruby
81
+ client.classrooms
82
+ ```
83
+ #### Enrollments
84
+ - Request all enrollments
85
+ ```ruby
86
+ client.enrollments
87
+ ```
88
+ - Request a subset of enrollments, filtered by their classroom's `id`:
89
+ ```ruby
90
+ client.enrollments([classroom_1['data']['id'], classroom_2['data']['id']], …)
91
+ ```
92
+
93
+ ## Development
94
+
95
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
96
+
97
+ 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).
98
+
99
+ ## Contributing
100
+
101
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/clever.
102
+
103
+ ## License
104
+
105
+ 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,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'bundler/setup'
6
+ require 'clever'
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ require 'pry'
13
+ Pry.start
14
+
15
+ require 'irb'
16
+ 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, 300) =~ /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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'clever/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'clever'
9
+ spec.version = Clever::VERSION
10
+ spec.authors = ['Robert Julius']
11
+ spec.email = ['robertmjulius@gmail.com']
12
+
13
+ spec.summary = 'Wrapper for the Clever API.'
14
+ spec.description = 'Wrapper for the Clever API.'
15
+ spec.homepage = 'https://github.com/tci/clever'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
+ else
23
+ fail 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
24
+ end
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ end
31
+ spec.bindir = 'exe'
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_runtime_dependency 'faraday'
36
+ spec.add_runtime_dependency 'faraday_middleware'
37
+
38
+ spec.add_development_dependency 'bundler', '~> 2.1.4'
39
+ spec.add_development_dependency 'mocha'
40
+ spec.add_development_dependency 'pry'
41
+ spec.add_development_dependency 'pry-nav'
42
+ spec.add_development_dependency 'rake', '~> 10.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'rspec_junit_formatter'
45
+ spec.add_development_dependency 'rubocop'
46
+ spec.add_development_dependency 'simplecov'
47
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+
6
+ require 'clever/client'
7
+ require 'clever/connection'
8
+ require 'clever/paginator'
9
+ require 'clever/response'
10
+ require 'clever/version'
11
+
12
+ require 'clever/types/base'
13
+ require 'clever/types/classroom'
14
+ require 'clever/types/course'
15
+ require 'clever/types/enrollment'
16
+ require 'clever/types/event'
17
+ require 'clever/types/student'
18
+ require 'clever/types/section'
19
+ require 'clever/types/teacher'
20
+ require 'clever/types/token'
21
+
22
+ module Clever
23
+ API_URL = 'https://api.clever.com/v2.0'
24
+ TOKENS_ENDPOINT = 'https://clever.com/oauth/tokens?owner_type=district'
25
+ STUDENTS_ENDPOINT = '/v2.0/students'
26
+ COURSES_ENDPOINT = '/v2.0/courses'
27
+ SECTIONS_ENDPOINT = '/v2.0/sections'
28
+ TEACHERS_ENDPOINT = '/v2.0/teachers'
29
+ EVENTS_ENDPOINT = '/v1.2/events'
30
+ GRADES_ENDPOINT = 'https://grades-api.beta.clever.com/v1/grade'
31
+
32
+ class DistrictNotFound < StandardError; end
33
+ class ConnectionError < StandardError; end
34
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clever
4
+ class Client
5
+ attr_accessor :app_id, :app_token, :sync_id, :logger,
6
+ :vendor_key, :vendor_secret, :username_source
7
+
8
+ attr_reader :api_url, :tokens_endpoint
9
+
10
+ def initialize
11
+ @api_url = API_URL
12
+ @tokens_endpoint = TOKENS_ENDPOINT
13
+ end
14
+
15
+ def self.configure
16
+ client = new
17
+ yield(client) if block_given?
18
+ client
19
+ end
20
+
21
+ def authenticate(app_id = @app_id)
22
+ return if @app_token
23
+
24
+ response = tokens
25
+
26
+ fail ConnectionError, response.raw_body unless response.success?
27
+
28
+ set_token(response, app_id)
29
+ end
30
+
31
+ def connection
32
+ @connection ||= Connection.new(self)
33
+ end
34
+
35
+ def tokens
36
+ response = connection.execute(@tokens_endpoint)
37
+ map_response!(response, Types::Token)
38
+ response
39
+ end
40
+
41
+ def most_recent_event
42
+ authenticate
43
+
44
+ endpoint = "#{Clever::EVENTS_ENDPOINT}?ending_before=last&limit=1"
45
+
46
+ event = @connection.execute(endpoint).body[0]
47
+ Types::Event.new(event['data']) if event
48
+ end
49
+
50
+ def events(starting_after)
51
+ authenticate
52
+
53
+ endpoint = "#{Clever::EVENTS_ENDPOINT}?starting_after=#{starting_after}"
54
+ Paginator.fetch(connection, endpoint, :get, Types::Event, client: self).force
55
+ end
56
+
57
+ %i(students courses teachers sections).each do |record_type|
58
+ define_method(record_type) do |record_uids = []|
59
+ authenticate
60
+
61
+ endpoint = Clever.const_get("#{record_type.upcase}_ENDPOINT")
62
+ type = Types.const_get(record_type.to_s.capitalize[0..-2])
63
+
64
+ records = Paginator.fetch(connection, endpoint, :get, type, client: self).force
65
+
66
+ return records if record_uids.empty?
67
+
68
+ records.select { |record| record_uids.to_set.include?(record.uid) }
69
+ end
70
+ end
71
+
72
+ # discard params to make the API behave the same as the one roster gem
73
+ def classrooms(*)
74
+ authenticate
75
+
76
+ fetched_courses = courses
77
+
78
+ sections.map do |section|
79
+ course = fetched_courses.find { |clever_course| clever_course.uid == section.course }
80
+ Types::Classroom.new(
81
+ 'id' => section.uid,
82
+ 'name' => section.name,
83
+ 'period' => section.period,
84
+ 'course_number' => course&.number,
85
+ 'grades' => section.grades
86
+ )
87
+ end
88
+ end
89
+
90
+ def enrollments(classroom_uids = [])
91
+ authenticate
92
+
93
+ fetched_sections = sections
94
+
95
+ enrollments = parse_enrollments(classroom_uids, fetched_sections)
96
+
97
+ p "Found #{enrollments.values.flatten.length} enrollments."
98
+
99
+ enrollments
100
+ end
101
+
102
+ def send_grade(request_body)
103
+ authenticate
104
+
105
+ @connection.execute(GRADES_ENDPOINT, :post, nil, request_body)
106
+ end
107
+
108
+ private
109
+
110
+ def parse_enrollments(classroom_uids, sections)
111
+ sections.each_with_object(student: [], teacher: []) do |section, enrollments|
112
+ next if classroom_uids.any? && !classroom_uids.include?(section.uid)
113
+
114
+ parse_student_enrollments!(section, enrollments)
115
+ parse_teacher_enrollments!(section, enrollments)
116
+ end
117
+ end
118
+
119
+ def parse_student_enrollments!(section, enrollments)
120
+ section.students.each do |student_uid|
121
+ enrollments[:student] << Types::Enrollment.new(
122
+ 'classroom_uid' => section.uid,
123
+ 'user_uid' => student_uid
124
+ )
125
+ end
126
+ end
127
+
128
+ def parse_teacher_enrollments!(section, enrollments)
129
+ section.teachers.each do |teacher_uid|
130
+ enrollments[:teacher] << Types::Enrollment.new(
131
+ 'classroom_uid' => section.uid,
132
+ 'user_uid' => teacher_uid
133
+ )
134
+ end
135
+ end
136
+
137
+ def set_token(tokens, app_id)
138
+ district_token = tokens.body.find { |district| district.owner['id'] == app_id }
139
+
140
+ fail DistrictNotFound unless district_token
141
+
142
+ connection.set_token(district_token.access_token)
143
+
144
+ @app_token = district_token.access_token
145
+ end
146
+
147
+ def map_response!(response, type)
148
+ response.body = map_response(type, response.body) if response.success?
149
+ end
150
+
151
+ def map_response(type, data)
152
+ data.map { |item_data| type.new(item_data) }
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clever
4
+ class Connection
5
+ OPEN_TIMEOUT = 60
6
+ TIMEOUT = 120
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ def execute(path, method = :get, params = nil, body = nil)
13
+ Response.new(raw_request(path, method, params, body))
14
+ end
15
+
16
+ def set_token(token)
17
+ connection.authorization :Bearer, token
18
+ end
19
+
20
+ def connection
21
+ return @connection if @connection
22
+
23
+ @connection = Faraday.new(@client.api_url) do |connection|
24
+ connection.request :json
25
+ connection.response :logger, @client.logger if @client.logger
26
+ connection.response :json, content_type: /\bjson$/
27
+ connection.adapter Faraday.default_adapter
28
+ end
29
+ @connection.basic_auth(@client.vendor_key, @client.vendor_secret)
30
+ @connection
31
+ end
32
+
33
+ def log(message = '')
34
+ return unless @client.logger
35
+
36
+ @client.logger.info message
37
+ end
38
+
39
+ private
40
+
41
+ def raw_request(path, method, params, body)
42
+ p "request #{path} #{params}"
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.body = body
49
+ end
50
+ end
51
+ end
52
+ end