learnosity-sdk 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/CONTRIBUTING.md +13 -1
- data/ChangeLog.md +28 -0
- data/README.md +8 -1
- data/REFERENCE.md +65 -5
- data/docs/quickstart/lrn-sdk-rails/Gemfile +15 -14
- data/docs/quickstart/lrn-sdk-rails/app/controllers/author_controller.rb +2 -2
- data/docs/quickstart/lrn-sdk-rails/app/controllers/data_api_controller.rb +156 -0
- data/docs/quickstart/lrn-sdk-rails/app/controllers/index_controller.rb +1 -1
- data/docs/quickstart/lrn-sdk-rails/app/controllers/items_controller.rb +3 -3
- data/docs/quickstart/lrn-sdk-rails/app/controllers/questions_controller.rb +2 -2
- data/docs/quickstart/lrn-sdk-rails/app/controllers/reports_controller.rb +4 -4
- data/docs/quickstart/lrn-sdk-rails/app/views/data_api/index.html.erb +277 -0
- data/docs/quickstart/lrn-sdk-rails/app/views/index/index.html.erb +4 -0
- data/docs/quickstart/lrn-sdk-rails/config/boot.rb +4 -0
- data/docs/quickstart/lrn-sdk-rails/config/initializers/new_framework_defaults.rb +2 -1
- data/docs/quickstart/lrn-sdk-rails/config/routes.rb +1 -0
- data/examples/simple/data_api_example.rb +82 -0
- data/examples/simple/uuid_example.rb +29 -0
- data/lib/learnosity/sdk/request/data_api.rb +196 -0
- data/lib/learnosity/sdk/utils/uuid.rb +21 -0
- data/lib/learnosity/sdk/version.rb +1 -1
- data/lib/learnosity/sdk.rb +7 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f6ae3d63eaf2bc30203e980a98b7636cefb5e760a9decf38b6b2f41650b8158
|
|
4
|
+
data.tar.gz: 94bdb3fa2b8bad774f4b06709aa89aba682c02ea871d28b51e3b02568ad11981
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d9de43a26ce21bf8dc476357ecf35149ad4145836e83458575451b47e86eb1e28417fe4a5709f018afc58b8cd569e70a235006e014deba40a9d9d49ec04ab7d
|
|
7
|
+
data.tar.gz: 60d57e9e7366fd2809b0c82029f1a3583918abbb45deb0c259165b23a8d9c7f244c8bfee873fb8f91d0c9de67be20c95090e877a9a0af3912dc7a1c7323f7acb
|
data/.travis.yml
CHANGED
data/CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Contributing
|
|
2
2
|
|
|
3
|
-
Contribution in the form of [
|
|
3
|
+
Contribution in the form of [PRs] are welcome.
|
|
4
|
+
|
|
5
|
+
## Why We Are No Longer Accepting Public Issues
|
|
6
|
+
After careful consideration, we’ve decided to discontinue accepting issues via GitHub Issues for our public repositories.
|
|
7
|
+
|
|
8
|
+
Here’s why:
|
|
9
|
+
|
|
10
|
+
- We have established support channels specifically designed to handle customer inquiries and issues.
|
|
11
|
+
- These channels are staffed 24/7, and we work diligently to ensure prompt responses and high-quality support.
|
|
12
|
+
- Maintaining and responding to GitHub Issues requires significant resources, and we are unable to provide the same level of support through this channel as we do through our dedicated support teams.
|
|
13
|
+
- By focusing on our dedicated support channels, we can streamline our processes and offer a more effective and responsive service to our users.
|
|
14
|
+
|
|
15
|
+
For any issues or support needs, please use the existing support channels. This will help us provide you with the best possible assistance in a timely manner.
|
|
4
16
|
|
|
5
17
|
## Testing
|
|
6
18
|
|
data/ChangeLog.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [v0.4.0] - 2026-01-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- UUID generation utility (`Learnosity::Sdk::Uuid.generate()`) for feature parity with Python and Node.js SDKs
|
|
15
|
+
- Data API support with dedicated `DataApi` class
|
|
16
|
+
- `request()` method for single authenticated Data API requests
|
|
17
|
+
- `request_iter()` method for iterating through paginated responses
|
|
18
|
+
- `results_iter()` method for iterating through individual results across pages
|
|
19
|
+
- Automatic routing metadata headers: `X-Learnosity-Consumer`, `X-Learnosity-Action`, `X-Learnosity-SDK`
|
|
20
|
+
- HTTP timeout configurations (15s open timeout, 60s read timeout)
|
|
21
|
+
- Data API demo added to Rails quickstart application
|
|
22
|
+
- Comprehensive unit and integration tests for Data API functionality
|
|
23
|
+
- Example usage in `examples/simple/data_api_example.rb`
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Updated documentation and examples to use `Learnosity::Sdk::Uuid.generate` instead of `SecureRandom.uuid`
|
|
28
|
+
- Improved error handling in Data API controller with detailed error messages and backtraces
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Ruby 2.6 compatibility in Rails quickstart (commented out `spring` gems that require Ruby 2.7+)
|
|
33
|
+
- Rails 6.1 compatibility with Ruby 2.6 (added `require 'logger'` to `config/boot.rb`)
|
|
34
|
+
- Bumped 3rd party libraries to fix known vulnerabilities in the quick start application
|
|
35
|
+
- Fixed seed data for the api-reports example in the quick start application
|
|
36
|
+
- Code quality improvements addressing Codacy findings
|
|
37
|
+
|
|
10
38
|
## [v0.3.0] - 2024-07-12
|
|
11
39
|
### Added
|
|
12
40
|
- Add support for api-authoraide.
|
data/README.md
CHANGED
|
@@ -98,6 +98,9 @@ For production use, you should install the SDK using the RubyGems package manage
|
|
|
98
98
|
Let's take a look at a simple example of the SDK in action. In this example, we'll load an assessment into the browser.
|
|
99
99
|
|
|
100
100
|
### **Start up your web server and view the standalone assessment example**
|
|
101
|
+
|
|
102
|
+
**Note:** The Rails quickstart supports Ruby 2.6+. The `spring` and `spring-watcher-listen` gems are commented out in the Gemfile for Ruby 2.6 compatibility. If you're using Ruby 2.7+, you can uncomment these gems for faster development reloading.
|
|
103
|
+
|
|
101
104
|
To start up your Ruby web server, first find the following folder location under the SDK. Change directory ('cd') to this location on the command line.
|
|
102
105
|
|
|
103
106
|
``` bash
|
|
@@ -107,6 +110,7 @@ To start up your Ruby web server, first find the following folder location under
|
|
|
107
110
|
To start, run this command from that folder:
|
|
108
111
|
|
|
109
112
|
``` bash
|
|
113
|
+
bundle install
|
|
110
114
|
rails server
|
|
111
115
|
```
|
|
112
116
|
|
|
@@ -143,7 +147,7 @@ We start by including some LearnositySDK helpers in [items_controller.rb](docs/q
|
|
|
143
147
|
|
|
144
148
|
``` ruby
|
|
145
149
|
require 'learnosity/sdk/request/init' # Learnosity helper.
|
|
146
|
-
require '
|
|
150
|
+
require 'learnosity/sdk' # For UUID generation utility.
|
|
147
151
|
```
|
|
148
152
|
|
|
149
153
|
Now we'll declare the configuration options for Items API. The following options specify which assessment content should be rendered, how it should be displayed, which user is taking this assessment and how their responses should be stored.
|
|
@@ -277,6 +281,9 @@ Take a look at some more in-depth options and tutorials on using Learnosity asse
|
|
|
277
281
|
### **SDK reference**
|
|
278
282
|
See a more detailed breakdown of all the SDK features, and examples of how to use more advanced or specialised features on the [SDK reference page](REFERENCE.md).
|
|
279
283
|
|
|
284
|
+
### **Data API support**
|
|
285
|
+
The SDK now includes comprehensive Data API support with automatic request signing, routing metadata headers, and pagination support. See the [Data API documentation](docs/DataApi.md) for detailed usage examples and API reference.
|
|
286
|
+
|
|
280
287
|
### **Additional quick start guides**
|
|
281
288
|
There are more quick start guides, going beyond the initial quick start topic of loading an assessment, these further tutorials show how to set up authoring and analytics:
|
|
282
289
|
|
data/REFERENCE.md
CHANGED
|
@@ -106,16 +106,67 @@ request = init.generate
|
|
|
106
106
|
Net::HTTP.post_form URI('https://data.learnosity.com/v1/itembank/items'), request
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
###
|
|
109
|
+
### DataApi Class (Recommended)
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
The SDK now includes a dedicated `DataApi` class that provides a more convenient way to interact with the Data API, including automatic pagination support, routing metadata headers, and simplified request handling.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
require 'learnosity/sdk/request/data_api'
|
|
115
|
+
|
|
116
|
+
# Initialize DataApi
|
|
117
|
+
data_api = Learnosity::Sdk::Request::DataApi.new(
|
|
118
|
+
consumer_key: 'your_consumer_key',
|
|
119
|
+
consumer_secret: 'your_consumer_secret',
|
|
120
|
+
domain: 'yourdomain.com'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
security_packet = {
|
|
124
|
+
'consumer_key' => 'your_consumer_key',
|
|
125
|
+
'domain' => 'yourdomain.com'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Make a single request
|
|
129
|
+
response = data_api.request(
|
|
130
|
+
'https://data.learnosity.com/v1/itembank/items',
|
|
131
|
+
security_packet,
|
|
132
|
+
'your_consumer_secret',
|
|
133
|
+
{ 'limit' => 10 },
|
|
134
|
+
'get'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Iterate through all pages automatically
|
|
138
|
+
data_api.request_iter(
|
|
139
|
+
'https://data.learnosity.com/v1/itembank/items',
|
|
140
|
+
security_packet,
|
|
141
|
+
'your_consumer_secret',
|
|
142
|
+
{ 'limit' => 100 },
|
|
143
|
+
'get'
|
|
144
|
+
).each do |page|
|
|
145
|
+
puts "Page has #{page['data'].length} items"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Iterate through individual results
|
|
149
|
+
data_api.results_iter(
|
|
150
|
+
'https://data.learnosity.com/v1/itembank/items',
|
|
151
|
+
security_packet,
|
|
152
|
+
'your_consumer_secret',
|
|
153
|
+
{ 'limit' => 100 },
|
|
154
|
+
'get'
|
|
155
|
+
).each do |item|
|
|
156
|
+
puts "Item: #{item['reference']}"
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See the [Data API documentation](docs/DataApi.md) for more details and examples.
|
|
161
|
+
|
|
162
|
+
### Recursive Queries (Legacy Approach)
|
|
112
163
|
|
|
113
164
|
Some requests are paginated to the `limit` passed in the request, or some
|
|
114
165
|
server-side default. Responses to those requests contain a `next` parameter in
|
|
115
166
|
their `meta` property, which can be placed in the next request to access another
|
|
116
167
|
page of data.
|
|
117
168
|
|
|
118
|
-
|
|
169
|
+
You can iterate through pages by looping over the
|
|
119
170
|
`Init#new`/`Init#generate`/`Net::HTTP#post_form`, updating the `next` attribute
|
|
120
171
|
in the request.
|
|
121
172
|
|
|
@@ -129,12 +180,21 @@ end
|
|
|
129
180
|
|
|
130
181
|
This will `require 'json'` to be able to parse the response.
|
|
131
182
|
|
|
183
|
+
**Note:** The new `DataApi` class (see above) handles pagination automatically and is the recommended approach.
|
|
184
|
+
|
|
132
185
|
See `examples/simple/init_data.rb` for an example.
|
|
133
186
|
|
|
134
187
|
### Generating UUIDs
|
|
135
188
|
|
|
136
|
-
You will need to generate UUIDs.
|
|
137
|
-
|
|
189
|
+
You will need to generate UUIDs for user IDs and session IDs. The SDK provides a convenient utility for this purpose.
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
require 'learnosity/sdk'
|
|
193
|
+
|
|
194
|
+
p Learnosity::Sdk::Uuid.generate
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Alternatively, you can use the Ruby `securerandom` module directly:
|
|
138
198
|
|
|
139
199
|
```ruby
|
|
140
200
|
require 'securerandom'
|
|
@@ -5,30 +5,29 @@ git_source(:github) do |repo_name|
|
|
|
5
5
|
"https://github.com/#{repo_name}.git"
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
|
10
|
-
gem 'rails', '~>
|
|
9
|
+
gem 'rails', '~> 6.1.0'
|
|
11
10
|
# Use sqlite3 as the database for Active Record
|
|
12
|
-
gem 'sqlite3', '~> 1.3
|
|
11
|
+
gem 'sqlite3', '~> 1.6.3'
|
|
13
12
|
# Use Puma as the app server
|
|
14
|
-
gem 'puma', '~> 4.
|
|
13
|
+
gem 'puma', '~> 6.4.0'
|
|
15
14
|
# Use SCSS for stylesheets
|
|
16
|
-
gem 'sass-rails', '~> 5.0'
|
|
15
|
+
gem 'sass-rails', '~> 5.1.0'
|
|
17
16
|
# Use Uglifier as compressor for JavaScript assets
|
|
18
|
-
gem 'uglifier', '>=
|
|
17
|
+
gem 'uglifier', '>= 4.2.0'
|
|
19
18
|
# Use CoffeeScript for .coffee assets and views
|
|
20
|
-
gem 'coffee-rails', '~>
|
|
19
|
+
gem 'coffee-rails', '~> 5.0'
|
|
21
20
|
# See https://github.com/rails/execjs#readme for more supported runtimes
|
|
22
|
-
# gem '
|
|
21
|
+
# gem 'mini_racer', platforms: :ruby
|
|
23
22
|
|
|
24
23
|
# Use jquery as the JavaScript library
|
|
25
24
|
gem 'jquery-rails'
|
|
26
25
|
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
|
|
27
26
|
gem 'turbolinks', '~> 5'
|
|
28
27
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
|
29
|
-
gem 'jbuilder', '~> 2.
|
|
28
|
+
gem 'jbuilder', '~> 2.11'
|
|
30
29
|
# Use Redis adapter to run Action Cable in production
|
|
31
|
-
# gem 'redis', '~>
|
|
30
|
+
# gem 'redis', '~> 4.0'
|
|
32
31
|
# Use ActiveModel has_secure_password
|
|
33
32
|
# gem 'bcrypt', '~> 3.1.7'
|
|
34
33
|
|
|
@@ -42,11 +41,13 @@ end
|
|
|
42
41
|
|
|
43
42
|
group :development do
|
|
44
43
|
# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
|
|
45
|
-
gem 'web-console', '>=
|
|
46
|
-
gem 'listen', '~> 3.
|
|
44
|
+
gem 'web-console', '>= 4.2.0'
|
|
45
|
+
gem 'listen', '~> 3.8'
|
|
47
46
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
# Note: spring-watcher-listen requires Ruby >= 2.7.0, so it's commented out for Ruby 2.6 compatibility
|
|
48
|
+
# If you're using Ruby 2.7+, you can uncomment these lines for faster development reloading:
|
|
49
|
+
# gem 'spring'
|
|
50
|
+
# gem 'spring-watcher-listen', '~> 2.1.0'
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require 'learnosity/sdk/request/init' # Learnosity helper.
|
|
2
|
-
require '
|
|
2
|
+
require 'learnosity/sdk' # For UUID generation utility.
|
|
3
3
|
|
|
4
4
|
class AuthorController < ApplicationController
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ class AuthorController < ApplicationController
|
|
|
7
7
|
# XXX: This is a Learnosity Demos consumer; replace it with your own consumer key. Set values in application.rb.
|
|
8
8
|
'consumer_key' => Rails.configuration.consumer_key,
|
|
9
9
|
'domain' => 'localhost',
|
|
10
|
-
'user_id' =>
|
|
10
|
+
'user_id' => Learnosity::Sdk::Uuid.generate
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
# XXX: The consumer secret should be in a properly secured credential store, and *NEVER* checked into version control
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require 'learnosity/sdk/request/data_api'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
class DataApiController < ApplicationController
|
|
5
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
6
|
+
# Note: This is a demo/quickstart controller that intentionally demonstrates
|
|
7
|
+
# three different Data API usage patterns (manual iteration, page iteration,
|
|
8
|
+
# and results iteration) with comprehensive error handling for educational purposes.
|
|
9
|
+
def index
|
|
10
|
+
# Initialize DataApi
|
|
11
|
+
data_api = Learnosity::Sdk::Request::DataApi.new(
|
|
12
|
+
consumer_key: Rails.configuration.consumer_key,
|
|
13
|
+
consumer_secret: Rails.configuration.consumer_secret,
|
|
14
|
+
domain: 'localhost'
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Endpoint and security packet
|
|
18
|
+
itembank_uri = 'https://data.learnosity.com/latest-lts/itembank/items'
|
|
19
|
+
security_packet = {
|
|
20
|
+
'consumer_key' => Rails.configuration.consumer_key,
|
|
21
|
+
'domain' => 'localhost'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Get SDK version
|
|
25
|
+
sdk_version = Learnosity::Sdk::VERSION
|
|
26
|
+
|
|
27
|
+
# Initialize request metadata
|
|
28
|
+
@request_metadata = {
|
|
29
|
+
endpoint: itembank_uri,
|
|
30
|
+
action: 'get',
|
|
31
|
+
status_code: nil,
|
|
32
|
+
headers: {
|
|
33
|
+
'X-Learnosity-Consumer' => data_api.extract_consumer(security_packet),
|
|
34
|
+
'X-Learnosity-Action' => data_api.derive_action(itembank_uri, 'get'),
|
|
35
|
+
'X-Learnosity-SDK' => "Ruby:#{sdk_version}"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Demo 1: Manual iteration (5 items)
|
|
40
|
+
@demo1_output = []
|
|
41
|
+
@demo1_error = nil
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
data_request = { 'limit' => 1 }
|
|
45
|
+
|
|
46
|
+
5.times do |i|
|
|
47
|
+
result = data_api.request(
|
|
48
|
+
itembank_uri,
|
|
49
|
+
security_packet,
|
|
50
|
+
Rails.configuration.consumer_secret,
|
|
51
|
+
data_request,
|
|
52
|
+
'get'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Capture status code from the first request
|
|
56
|
+
@request_metadata[:status_code] = result.code if i == 0
|
|
57
|
+
|
|
58
|
+
response = JSON.parse(result.body) rescue { 'raw_body' => result.body }
|
|
59
|
+
|
|
60
|
+
if response['data'] && response['data'].length > 0
|
|
61
|
+
item = response['data'][0]
|
|
62
|
+
@demo1_output << {
|
|
63
|
+
number: i + 1,
|
|
64
|
+
reference: item['reference'] || 'N/A',
|
|
65
|
+
status: item['status'] || 'N/A'
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if response['meta'] && response['meta']['next']
|
|
70
|
+
data_request = { 'next' => response['meta']['next'] }
|
|
71
|
+
else
|
|
72
|
+
break
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
rescue => e
|
|
76
|
+
@demo1_error = {
|
|
77
|
+
error: "#{e.class}: #{e.message}",
|
|
78
|
+
backtrace: e.backtrace&.first(5)
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Demo 2: Page iteration (5 pages)
|
|
83
|
+
@demo2_output = []
|
|
84
|
+
@demo2_error = nil
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
data_request = { 'limit' => 1 }
|
|
88
|
+
page_count = 0
|
|
89
|
+
|
|
90
|
+
data_api.request_iter(
|
|
91
|
+
itembank_uri,
|
|
92
|
+
security_packet,
|
|
93
|
+
Rails.configuration.consumer_secret,
|
|
94
|
+
data_request,
|
|
95
|
+
'get'
|
|
96
|
+
).each do |page|
|
|
97
|
+
page_count += 1
|
|
98
|
+
page_data = {
|
|
99
|
+
page_number: page_count,
|
|
100
|
+
item_count: page['data'] ? page['data'].length : 0,
|
|
101
|
+
items: []
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if page['data']
|
|
105
|
+
page['data'].each do |item|
|
|
106
|
+
page_data[:items] << {
|
|
107
|
+
reference: item['reference'] || 'N/A',
|
|
108
|
+
status: item['status'] || 'N/A'
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@demo2_output << page_data
|
|
114
|
+
break if page_count >= 5
|
|
115
|
+
end
|
|
116
|
+
rescue => e
|
|
117
|
+
@demo2_error = {
|
|
118
|
+
error: "#{e.class}: #{e.message}",
|
|
119
|
+
backtrace: e.backtrace&.first(5)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Demo 3: Results iteration (5 items)
|
|
124
|
+
@demo3_output = []
|
|
125
|
+
@demo3_error = nil
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
data_request = { 'limit' => 1 }
|
|
129
|
+
result_count = 0
|
|
130
|
+
|
|
131
|
+
data_api.results_iter(
|
|
132
|
+
itembank_uri,
|
|
133
|
+
security_packet,
|
|
134
|
+
Rails.configuration.consumer_secret,
|
|
135
|
+
data_request,
|
|
136
|
+
'get'
|
|
137
|
+
).each do |item|
|
|
138
|
+
result_count += 1
|
|
139
|
+
@demo3_output << {
|
|
140
|
+
number: result_count,
|
|
141
|
+
reference: item['reference'] || 'N/A',
|
|
142
|
+
status: item['status'] || 'N/A',
|
|
143
|
+
json: JSON.pretty_generate(item)[0..500]
|
|
144
|
+
}
|
|
145
|
+
break if result_count >= 5
|
|
146
|
+
end
|
|
147
|
+
rescue => e
|
|
148
|
+
@demo3_error = {
|
|
149
|
+
error: "#{e.class}: #{e.message}",
|
|
150
|
+
backtrace: e.backtrace&.first(5)
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
155
|
+
end
|
|
156
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require 'learnosity/sdk/request/init' # Learnosity helper.
|
|
2
|
-
require '
|
|
2
|
+
require 'learnosity/sdk' # For UUID generation utility.
|
|
3
3
|
|
|
4
4
|
class ItemsController < ApplicationController
|
|
5
5
|
@@security_packet = {
|
|
@@ -12,9 +12,9 @@ class ItemsController < ApplicationController
|
|
|
12
12
|
@@consumer_secret = Rails.configuration.consumer_secret
|
|
13
13
|
|
|
14
14
|
@@items_request = {
|
|
15
|
-
"user_id" =>
|
|
15
|
+
"user_id" => Learnosity::Sdk::Uuid.generate,
|
|
16
16
|
"activity_template_id" => "quickstart_examples_activity_template_001",
|
|
17
|
-
"session_id" =>
|
|
17
|
+
"session_id" => Learnosity::Sdk::Uuid.generate,
|
|
18
18
|
"activity_id" => "quickstart_examples_activity_001",
|
|
19
19
|
"rendering_type" => "assess",
|
|
20
20
|
"type" => "submit_practice",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require 'learnosity/sdk/request/init' # Learnosity helper.
|
|
2
|
-
require '
|
|
2
|
+
require 'learnosity/sdk' # For UUID generation utility.
|
|
3
3
|
|
|
4
4
|
class QuestionsController < ApplicationController
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ class QuestionsController < ApplicationController
|
|
|
7
7
|
# XXX: This is a Learnosity Demos consumer; replace it with your own consumer key. Set values in application.rb.
|
|
8
8
|
'consumer_key' => Rails.configuration.consumer_key,
|
|
9
9
|
'domain' => 'localhost',
|
|
10
|
-
'user_id' =>
|
|
10
|
+
'user_id' => Learnosity::Sdk::Uuid.generate
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
# XXX: The consumer secret should be in a properly secured credential store, and *NEVER* checked into version control
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require 'learnosity/sdk/request/init' # Learnosity helper.
|
|
2
|
-
require '
|
|
2
|
+
require 'learnosity/sdk' # For UUID generation utility.
|
|
3
3
|
|
|
4
4
|
class ReportsController < ApplicationController
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ class ReportsController < ApplicationController
|
|
|
7
7
|
# XXX: This is a Learnosity Demos consumer; replace it with your own consumer key. Set values in application.rb.
|
|
8
8
|
'consumer_key' => Rails.configuration.consumer_key,
|
|
9
9
|
'domain' => 'localhost',
|
|
10
|
-
'user_id' =>
|
|
10
|
+
'user_id' => Learnosity::Sdk::Uuid.generate
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
# XXX: The consumer secret should be in a properly secured credential store, and *NEVER* checked into version control
|
|
@@ -17,8 +17,8 @@ class ReportsController < ApplicationController
|
|
|
17
17
|
"reports" => [{
|
|
18
18
|
"id"=> "session-detail",
|
|
19
19
|
"type"=> "session-detail-by-item",
|
|
20
|
-
"user_id"=> "
|
|
21
|
-
"session_id"=> "
|
|
20
|
+
"user_id"=> "student_0001",
|
|
21
|
+
"session_id"=> "ef4f80b8-e281-41f4-9efd-349b7eb9dd37"
|
|
22
22
|
}]
|
|
23
23
|
}
|
|
24
24
|
def index
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<style>
|
|
5
|
+
body {
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
7
|
+
max-width: 1200px;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
padding: 20px;
|
|
10
|
+
background: #f5f5f5;
|
|
11
|
+
}
|
|
12
|
+
h1 {
|
|
13
|
+
color: #333;
|
|
14
|
+
border-bottom: 3px solid #667eea;
|
|
15
|
+
padding-bottom: 10px;
|
|
16
|
+
}
|
|
17
|
+
.demo-section {
|
|
18
|
+
background: #f8f9fa;
|
|
19
|
+
border-left: 4px solid #667eea;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
margin: 20px 0;
|
|
22
|
+
border-radius: 4px;
|
|
23
|
+
}
|
|
24
|
+
.demo-section h2 {
|
|
25
|
+
margin-top: 0;
|
|
26
|
+
color: #333;
|
|
27
|
+
}
|
|
28
|
+
.item {
|
|
29
|
+
background: white;
|
|
30
|
+
padding: 15px;
|
|
31
|
+
margin: 10px 0;
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
border: 1px solid #dee2e6;
|
|
34
|
+
}
|
|
35
|
+
.item-reference {
|
|
36
|
+
font-family: monospace;
|
|
37
|
+
color: #667eea;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
margin-bottom: 5px;
|
|
40
|
+
}
|
|
41
|
+
.item-status {
|
|
42
|
+
color: #666;
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
}
|
|
45
|
+
.meta-info {
|
|
46
|
+
background: #e7f3ff;
|
|
47
|
+
padding: 10px;
|
|
48
|
+
margin: 10px 0;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
border-left: 3px solid #0066cc;
|
|
51
|
+
}
|
|
52
|
+
.error {
|
|
53
|
+
background: #f8d7da;
|
|
54
|
+
color: #721c24;
|
|
55
|
+
padding: 15px;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
border-left: 4px solid #f5c6cb;
|
|
58
|
+
}
|
|
59
|
+
pre {
|
|
60
|
+
background: #2d2d2d;
|
|
61
|
+
color: #f8f8f2;
|
|
62
|
+
padding: 15px;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
overflow-x: auto;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
}
|
|
67
|
+
code {
|
|
68
|
+
background: #f4f4f4;
|
|
69
|
+
padding: 2px 6px;
|
|
70
|
+
border-radius: 3px;
|
|
71
|
+
font-family: monospace;
|
|
72
|
+
}
|
|
73
|
+
.note-box {
|
|
74
|
+
background: #d1ecf1;
|
|
75
|
+
border: 1px solid #bee5eb;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
padding: 15px;
|
|
78
|
+
margin: 20px 0;
|
|
79
|
+
color: #0c5460;
|
|
80
|
+
}
|
|
81
|
+
.note-box strong {
|
|
82
|
+
display: block;
|
|
83
|
+
margin-bottom: 5px;
|
|
84
|
+
}
|
|
85
|
+
.api-responses {
|
|
86
|
+
margin: 20px 0;
|
|
87
|
+
}
|
|
88
|
+
.api-responses h2 {
|
|
89
|
+
color: #333;
|
|
90
|
+
margin-bottom: 15px;
|
|
91
|
+
}
|
|
92
|
+
.request-info {
|
|
93
|
+
background: #4a90e2;
|
|
94
|
+
color: white;
|
|
95
|
+
padding: 12px 20px;
|
|
96
|
+
border-radius: 4px 4px 0 0;
|
|
97
|
+
font-weight: bold;
|
|
98
|
+
margin-top: 20px;
|
|
99
|
+
}
|
|
100
|
+
.request-details {
|
|
101
|
+
background: white;
|
|
102
|
+
border: 1px solid #dee2e6;
|
|
103
|
+
border-top: none;
|
|
104
|
+
}
|
|
105
|
+
.request-row {
|
|
106
|
+
display: flex;
|
|
107
|
+
border-bottom: 1px solid #dee2e6;
|
|
108
|
+
}
|
|
109
|
+
.request-row:last-child {
|
|
110
|
+
border-bottom: none;
|
|
111
|
+
}
|
|
112
|
+
.request-label {
|
|
113
|
+
background: #f8f9fa;
|
|
114
|
+
padding: 12px 20px;
|
|
115
|
+
font-weight: bold;
|
|
116
|
+
width: 150px;
|
|
117
|
+
border-right: 1px solid #dee2e6;
|
|
118
|
+
}
|
|
119
|
+
.request-value {
|
|
120
|
+
padding: 12px 20px;
|
|
121
|
+
flex: 1;
|
|
122
|
+
font-family: monospace;
|
|
123
|
+
word-break: break-all;
|
|
124
|
+
}
|
|
125
|
+
.request-value.success {
|
|
126
|
+
color: #28a745;
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
}
|
|
129
|
+
.request-value.url {
|
|
130
|
+
color: #e83e8c;
|
|
131
|
+
}
|
|
132
|
+
.request-value.action {
|
|
133
|
+
color: #e83e8c;
|
|
134
|
+
}
|
|
135
|
+
.metadata-headers {
|
|
136
|
+
background: #5cb85c;
|
|
137
|
+
color: white;
|
|
138
|
+
padding: 12px 20px;
|
|
139
|
+
border-radius: 4px 4px 0 0;
|
|
140
|
+
font-weight: bold;
|
|
141
|
+
margin-top: 20px;
|
|
142
|
+
}
|
|
143
|
+
.metadata-note {
|
|
144
|
+
background: white;
|
|
145
|
+
border: 1px solid #dee2e6;
|
|
146
|
+
border-top: none;
|
|
147
|
+
padding: 12px 20px;
|
|
148
|
+
color: #666;
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
}
|
|
151
|
+
.header-row {
|
|
152
|
+
display: flex;
|
|
153
|
+
border-bottom: 1px solid #dee2e6;
|
|
154
|
+
background: white;
|
|
155
|
+
}
|
|
156
|
+
.header-row:last-child {
|
|
157
|
+
border-bottom: none;
|
|
158
|
+
}
|
|
159
|
+
.header-name {
|
|
160
|
+
background: #f8f9fa;
|
|
161
|
+
padding: 12px 20px;
|
|
162
|
+
font-weight: bold;
|
|
163
|
+
width: 250px;
|
|
164
|
+
border-right: 1px solid #dee2e6;
|
|
165
|
+
color: #e83e8c;
|
|
166
|
+
}
|
|
167
|
+
.header-value {
|
|
168
|
+
padding: 12px 20px;
|
|
169
|
+
flex: 1;
|
|
170
|
+
font-family: monospace;
|
|
171
|
+
word-break: break-all;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<h1>Data API Example - With Metadata Headers</h1>
|
|
177
|
+
|
|
178
|
+
<div class="note-box">
|
|
179
|
+
<strong>Note:</strong> This example demonstrates the Data API with automatic metadata headers. Every request includes consumer, action, and SDK language-version information.
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<% if @request_metadata %>
|
|
183
|
+
<div class="api-responses">
|
|
184
|
+
<h2>API Responses</h2>
|
|
185
|
+
|
|
186
|
+
<div class="request-info">Request Information</div>
|
|
187
|
+
<div class="request-details">
|
|
188
|
+
<div class="request-row">
|
|
189
|
+
<div class="request-label">Endpoint</div>
|
|
190
|
+
<div class="request-value url"><%= @request_metadata[:endpoint] %></div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="request-row">
|
|
193
|
+
<div class="request-label">Action</div>
|
|
194
|
+
<div class="request-value action"><%= @request_metadata[:action] %></div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="request-row">
|
|
197
|
+
<div class="request-label">Status Code</div>
|
|
198
|
+
<div class="request-value success"><%= @request_metadata[:status_code] || 'N/A' %></div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="metadata-headers">Metadata Headers (Sent Automatically)</div>
|
|
203
|
+
<div class="metadata-note">
|
|
204
|
+
These headers are added automatically by the SDK and are invisible to customers:
|
|
205
|
+
</div>
|
|
206
|
+
<div class="request-details">
|
|
207
|
+
<div class="header-row">
|
|
208
|
+
<div class="header-name">X-Learnosity-Consumer</div>
|
|
209
|
+
<div class="header-value"><%= @request_metadata[:headers]['X-Learnosity-Consumer'] %></div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="header-row">
|
|
212
|
+
<div class="header-name">X-Learnosity-Action</div>
|
|
213
|
+
<div class="header-value"><%= @request_metadata[:headers]['X-Learnosity-Action'] %></div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="header-row">
|
|
216
|
+
<div class="header-name">X-Learnosity-SDK</div>
|
|
217
|
+
<div class="header-value"><%= @request_metadata[:headers]['X-Learnosity-SDK'] %></div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<% end %>
|
|
222
|
+
|
|
223
|
+
<div class="demo-section">
|
|
224
|
+
<h2>Demo 1: Manual Iteration (5 items)</h2>
|
|
225
|
+
<p>Using <code>request()</code> method with manual pagination via the 'next' pointer.</p>
|
|
226
|
+
<% if @demo1_error %>
|
|
227
|
+
<div class="error">Error: <%= @demo1_error %></div>
|
|
228
|
+
<% else %>
|
|
229
|
+
<% @demo1_output.each do |item| %>
|
|
230
|
+
<div class="item">
|
|
231
|
+
<div class="item-reference">Item <%= item[:number] %>: <%= item[:reference] %></div>
|
|
232
|
+
<div class="item-status">Status: <%= item[:status] %></div>
|
|
233
|
+
</div>
|
|
234
|
+
<% end %>
|
|
235
|
+
<% end %>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div class="demo-section">
|
|
239
|
+
<h2>Demo 2: Page Iteration (5 pages)</h2>
|
|
240
|
+
<p>Using <code>request_iter()</code> method to automatically iterate over pages.</p>
|
|
241
|
+
<% if @demo2_error %>
|
|
242
|
+
<div class="error">Error: <%= @demo2_error %></div>
|
|
243
|
+
<% else %>
|
|
244
|
+
<% @demo2_output.each do |page| %>
|
|
245
|
+
<div class="meta-info">
|
|
246
|
+
Page <%= page[:page_number] %>: <%= page[:item_count] %> items
|
|
247
|
+
</div>
|
|
248
|
+
<% page[:items].each do |item| %>
|
|
249
|
+
<div class="item">
|
|
250
|
+
<div class="item-reference"><%= item[:reference] %></div>
|
|
251
|
+
<div class="item-status">Status: <%= item[:status] %></div>
|
|
252
|
+
</div>
|
|
253
|
+
<% end %>
|
|
254
|
+
<% end %>
|
|
255
|
+
<% end %>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="demo-section">
|
|
259
|
+
<h2>Demo 3: Results Iteration (5 items)</h2>
|
|
260
|
+
<p>Using <code>results_iter()</code> method to automatically iterate over individual items.</p>
|
|
261
|
+
<% if @demo3_error %>
|
|
262
|
+
<div class="error">Error: <%= @demo3_error %></div>
|
|
263
|
+
<% else %>
|
|
264
|
+
<% @demo3_output.each do |item| %>
|
|
265
|
+
<div class="item">
|
|
266
|
+
<div class="item-reference">Item <%= item[:number] %>: <%= h item[:reference] %></div>
|
|
267
|
+
<div class="item-status">Status: <%= h item[:status] %></div>
|
|
268
|
+
<pre><%= h item[:json] %>...</pre>
|
|
269
|
+
</div>
|
|
270
|
+
<% end %>
|
|
271
|
+
<% end %>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<p><a href="/">Back to API Examples</a></p>
|
|
275
|
+
</body>
|
|
276
|
+
</html>
|
|
277
|
+
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
<td>Authoraide API</td>
|
|
34
34
|
<td><%=link_to("Here", authoraide_index_path, target: '_blank') %> </td>
|
|
35
35
|
</tr>
|
|
36
|
+
<tr>
|
|
37
|
+
<td>Data API</td>
|
|
38
|
+
<td><%=link_to("Here", data_api_index_path, target: '_blank') %> </td>
|
|
39
|
+
</tr>
|
|
36
40
|
</table>
|
|
37
41
|
</body>
|
|
38
42
|
</html>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
|
2
2
|
|
|
3
3
|
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
|
4
|
+
|
|
5
|
+
# Fix for Ruby 2.6 compatibility with Rails 6.1
|
|
6
|
+
# Rails 6.1 expects Logger to be available before ActiveSupport loads
|
|
7
|
+
require 'logger'
|
|
@@ -18,7 +18,8 @@ ActiveSupport.to_time_preserves_timezone = true
|
|
|
18
18
|
Rails.application.config.active_record.belongs_to_required_by_default = true
|
|
19
19
|
|
|
20
20
|
# Do not halt callback chains when a callback returns false. Previous versions had true.
|
|
21
|
-
|
|
21
|
+
# This option is not applicable for Rails 6.1 and has been removed.
|
|
22
|
+
# ActiveSupport.halt_callback_chains_on_return_false = false
|
|
22
23
|
|
|
23
24
|
# Configure SSL options to enable HSTS with subdomains. Previous versions had false.
|
|
24
25
|
Rails.application.config.ssl_options = { hsts: { subdomains: true } }
|
|
@@ -6,6 +6,7 @@ Rails.application.routes.draw do
|
|
|
6
6
|
get 'authoraide/index' , as: 'authoraide_index'
|
|
7
7
|
get 'reports/index', as: 'reports_index'
|
|
8
8
|
get 'items/index', as: 'items_index'
|
|
9
|
+
get 'data_api/index', as: 'data_api_index'
|
|
9
10
|
# get 'abc' , to: "index#index"
|
|
10
11
|
|
|
11
12
|
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'learnosity/sdk/request/data_api'
|
|
3
|
+
|
|
4
|
+
# Configuration
|
|
5
|
+
# XXX: This is a Learnosity Demos consumer; replace it with your own consumer key
|
|
6
|
+
consumer_key = 'yis0TYCu7U9V4o7M'
|
|
7
|
+
# XXX: The consumer secret should be in a properly secured credential store, and *NEVER* checked into version control
|
|
8
|
+
consumer_secret = '74c5fd430cf1242a527f6223aebd42d30464be22'
|
|
9
|
+
domain = 'localhost'
|
|
10
|
+
|
|
11
|
+
# Initialize DataApi
|
|
12
|
+
data_api = Learnosity::Sdk::DataApi.new(
|
|
13
|
+
consumer_key: consumer_key,
|
|
14
|
+
consumer_secret: consumer_secret,
|
|
15
|
+
domain: domain
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Security packet
|
|
19
|
+
security_packet = {
|
|
20
|
+
'consumer_key' => consumer_key,
|
|
21
|
+
'domain' => domain
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Endpoint
|
|
25
|
+
endpoint = 'https://data.learnosity.com/v1/itembank/items'
|
|
26
|
+
|
|
27
|
+
puts "=== Example 1: Single Request ==="
|
|
28
|
+
puts
|
|
29
|
+
|
|
30
|
+
# Make a single request
|
|
31
|
+
response = data_api.request(
|
|
32
|
+
endpoint,
|
|
33
|
+
security_packet,
|
|
34
|
+
consumer_secret,
|
|
35
|
+
{ 'limit' => 5 },
|
|
36
|
+
'get'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
puts "Status: #{response.code}"
|
|
40
|
+
data = JSON.parse(response.body)
|
|
41
|
+
puts "Records: #{data['meta']['records']}"
|
|
42
|
+
puts "Items returned: #{data['data'].length}"
|
|
43
|
+
puts
|
|
44
|
+
|
|
45
|
+
puts "=== Example 2: Iterate Through Pages ==="
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
# Iterate through pages (up to 3 pages)
|
|
49
|
+
page_count = 0
|
|
50
|
+
data_api.request_iter(
|
|
51
|
+
endpoint,
|
|
52
|
+
security_packet,
|
|
53
|
+
consumer_secret,
|
|
54
|
+
{ 'limit' => 5 },
|
|
55
|
+
'get'
|
|
56
|
+
).each do |page|
|
|
57
|
+
page_count += 1
|
|
58
|
+
puts "Page #{page_count}: #{page['data'].length} items"
|
|
59
|
+
break if page_count >= 3
|
|
60
|
+
end
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
puts "=== Example 3: Iterate Through Individual Results ==="
|
|
64
|
+
puts
|
|
65
|
+
|
|
66
|
+
# Iterate through individual results (up to 10 items)
|
|
67
|
+
item_count = 0
|
|
68
|
+
data_api.results_iter(
|
|
69
|
+
endpoint,
|
|
70
|
+
security_packet,
|
|
71
|
+
consumer_secret,
|
|
72
|
+
{ 'limit' => 5 },
|
|
73
|
+
'get'
|
|
74
|
+
).each do |item|
|
|
75
|
+
item_count += 1
|
|
76
|
+
puts "Item #{item_count}: #{item['reference'] || item['id'] || 'N/A'}"
|
|
77
|
+
break if item_count >= 10
|
|
78
|
+
end
|
|
79
|
+
puts
|
|
80
|
+
|
|
81
|
+
puts "Done!"
|
|
82
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
require 'learnosity/sdk'
|
|
4
|
+
|
|
5
|
+
# Example: Generate UUIDs using the Learnosity SDK utility
|
|
6
|
+
# This is commonly used for user_id and session_id in API requests
|
|
7
|
+
|
|
8
|
+
puts "Generating UUIDs using Learnosity::Sdk::Uuid.generate:"
|
|
9
|
+
puts
|
|
10
|
+
|
|
11
|
+
# Generate a few UUIDs
|
|
12
|
+
5.times do |i|
|
|
13
|
+
uuid = Learnosity::Sdk::Uuid.generate
|
|
14
|
+
puts "UUID #{i + 1}: #{uuid}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
puts
|
|
18
|
+
puts "Example usage in API requests:"
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
# Example: Using UUIDs in an Items API request
|
|
22
|
+
user_id = Learnosity::Sdk::Uuid.generate
|
|
23
|
+
session_id = Learnosity::Sdk::Uuid.generate
|
|
24
|
+
|
|
25
|
+
puts "user_id: #{user_id}"
|
|
26
|
+
puts "session_id: #{session_id}"
|
|
27
|
+
|
|
28
|
+
# vim: sw=2
|
|
29
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'learnosity/sdk/request/init'
|
|
5
|
+
require 'learnosity/sdk/version'
|
|
6
|
+
|
|
7
|
+
module Learnosity
|
|
8
|
+
module Sdk
|
|
9
|
+
module Request
|
|
10
|
+
# DataApi - Routing layer for Learnosity Data API
|
|
11
|
+
#
|
|
12
|
+
# Provides methods to make HTTP requests to the Data API with automatic
|
|
13
|
+
# signing and pagination support.
|
|
14
|
+
class DataApi
|
|
15
|
+
attr_reader :consumer_key, :consumer_secret, :domain
|
|
16
|
+
|
|
17
|
+
# Initialize a new DataApi instance
|
|
18
|
+
#
|
|
19
|
+
# @param options [Hash] Configuration options
|
|
20
|
+
# @option options [String] :consumer_key Learnosity consumer key
|
|
21
|
+
# @option options [String] :consumer_secret Learnosity consumer secret
|
|
22
|
+
# @option options [String] :domain Domain for security packet
|
|
23
|
+
# @option options [Proc] :http_adapter Optional custom HTTP adapter
|
|
24
|
+
def initialize(options = {})
|
|
25
|
+
@consumer_key = options[:consumer_key]
|
|
26
|
+
@consumer_secret = options[:consumer_secret]
|
|
27
|
+
@domain = options[:domain]
|
|
28
|
+
@http_adapter = options[:http_adapter] || method(:default_http_adapter)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Make a single request to Data API
|
|
32
|
+
#
|
|
33
|
+
# @param endpoint [String] Full URL to the Data API endpoint
|
|
34
|
+
# @param security_packet [Hash] Security object with consumer_key and domain
|
|
35
|
+
# @param secret [String] Consumer secret
|
|
36
|
+
# @param request_packet [Hash] Request parameters (default: {})
|
|
37
|
+
# @param action [String] Action type: 'get', 'set', 'update', 'delete' (default: 'get')
|
|
38
|
+
# @return [Net::HTTPResponse] HTTP response object
|
|
39
|
+
def request(endpoint, security_packet, secret, request_packet = {}, action = 'get')
|
|
40
|
+
# Generate signed request using SDK
|
|
41
|
+
init = Init.new('data', security_packet, secret, request_packet, action)
|
|
42
|
+
signed_request = init.generate
|
|
43
|
+
|
|
44
|
+
# Extract metadata for routing
|
|
45
|
+
consumer = extract_consumer(security_packet)
|
|
46
|
+
derived_action = derive_action(endpoint, action)
|
|
47
|
+
|
|
48
|
+
# Prepare headers with routing metadata
|
|
49
|
+
headers = {
|
|
50
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
51
|
+
'X-Learnosity-Consumer' => consumer,
|
|
52
|
+
'X-Learnosity-Action' => derived_action,
|
|
53
|
+
'X-Learnosity-SDK' => "Ruby:#{Learnosity::Sdk::VERSION}"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Make HTTP request using adapter
|
|
57
|
+
@http_adapter.call(endpoint, signed_request, headers)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Iterate over pages of results from Data API
|
|
61
|
+
#
|
|
62
|
+
# @param endpoint [String] Full URL to the Data API endpoint
|
|
63
|
+
# @param security_packet [Hash] Security object
|
|
64
|
+
# @param secret [String] Consumer secret
|
|
65
|
+
# @param request_packet [Hash] Request parameters (default: {})
|
|
66
|
+
# @param action [String] Action type (default: 'get')
|
|
67
|
+
# @return [Enumerator] Enumerator yielding pages of results
|
|
68
|
+
def request_iter(endpoint, security_packet, secret, request_packet = {}, action = 'get')
|
|
69
|
+
Enumerator.new do |yielder|
|
|
70
|
+
# Deep copy to avoid mutation
|
|
71
|
+
security = deep_copy(security_packet)
|
|
72
|
+
request_params = deep_copy(request_packet)
|
|
73
|
+
data_end = false
|
|
74
|
+
|
|
75
|
+
until data_end
|
|
76
|
+
response = self.request(endpoint, security, secret, request_params, action)
|
|
77
|
+
validate_response(response)
|
|
78
|
+
|
|
79
|
+
data = parse_response_body(response)
|
|
80
|
+
validate_response_status(data)
|
|
81
|
+
|
|
82
|
+
data_end = !has_more_pages?(data)
|
|
83
|
+
request_params['next'] = data['meta']['next'] if data['meta'] && data['meta']['next']
|
|
84
|
+
|
|
85
|
+
yielder << data
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Iterate over individual results from Data API
|
|
91
|
+
#
|
|
92
|
+
# Automatically handles pagination and yields each individual result
|
|
93
|
+
# from the data array.
|
|
94
|
+
#
|
|
95
|
+
# @param endpoint [String] Full URL to the Data API endpoint
|
|
96
|
+
# @param security_packet [Hash] Security object
|
|
97
|
+
# @param secret [String] Consumer secret
|
|
98
|
+
# @param request_packet [Hash] Request parameters (default: {})
|
|
99
|
+
# @param action [String] Action type (default: 'get')
|
|
100
|
+
# @return [Enumerator] Enumerator yielding individual results
|
|
101
|
+
def results_iter(endpoint, security_packet, secret, request_packet = {}, action = 'get')
|
|
102
|
+
Enumerator.new do |yielder|
|
|
103
|
+
request_iter(endpoint, security_packet, secret, request_packet, action).each do |page|
|
|
104
|
+
if page['data'].is_a?(Hash)
|
|
105
|
+
# If data is a hash (not array), yield key-value pairs
|
|
106
|
+
page['data'].each do |key, value|
|
|
107
|
+
yielder << { key => value }
|
|
108
|
+
end
|
|
109
|
+
elsif page['data'].is_a?(Array)
|
|
110
|
+
# If data is an array, yield each item
|
|
111
|
+
page['data'].each do |result|
|
|
112
|
+
yielder << result
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Extract consumer key from security packet
|
|
120
|
+
def extract_consumer(security_packet)
|
|
121
|
+
security_packet['consumer_key'] || security_packet[:consumer_key] || ''
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Derive action metadata from endpoint and action
|
|
125
|
+
def derive_action(endpoint, action)
|
|
126
|
+
uri = URI.parse(endpoint)
|
|
127
|
+
path = uri.path.sub(/\/$/, '')
|
|
128
|
+
|
|
129
|
+
# Remove version prefix (e.g., /v1, /v2023.1.LTS, /latest)
|
|
130
|
+
path_parts = path.split('/')
|
|
131
|
+
|
|
132
|
+
if path_parts.length > 1
|
|
133
|
+
first_segment = path_parts[1].downcase
|
|
134
|
+
version_pattern = /^v[\d.]+(?:\.(lts|preview\d+))?$/
|
|
135
|
+
special_versions = ['latest', 'latest-lts', 'developer']
|
|
136
|
+
|
|
137
|
+
if version_pattern.match?(first_segment) || special_versions.include?(first_segment)
|
|
138
|
+
path = '/' + path_parts[2..-1].join('/')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
"#{action}_#{path}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Default HTTP adapter using Net::HTTP
|
|
148
|
+
def default_http_adapter(endpoint, signed_request, headers)
|
|
149
|
+
uri = URI.parse(endpoint)
|
|
150
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
151
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
152
|
+
http.open_timeout = 15 # seconds to establish connection
|
|
153
|
+
http.read_timeout = 60 # seconds to read response
|
|
154
|
+
|
|
155
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
156
|
+
request.set_form_data(signed_request)
|
|
157
|
+
|
|
158
|
+
http.request(request)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Deep copy a hash to avoid mutation
|
|
162
|
+
# Using JSON serialization instead of Marshal for security
|
|
163
|
+
def deep_copy(obj)
|
|
164
|
+
JSON.parse(JSON.generate(obj))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validate HTTP response
|
|
168
|
+
def validate_response(response)
|
|
169
|
+
return if response.is_a?(Net::HTTPSuccess)
|
|
170
|
+
raise "Server returned HTTP status #{response.code}: #{response.body}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Parse response body as JSON
|
|
174
|
+
def parse_response_body(response)
|
|
175
|
+
JSON.parse(response.body)
|
|
176
|
+
rescue JSON::ParserError
|
|
177
|
+
raise "Server returned invalid JSON: #{response.body}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Validate response has successful status
|
|
181
|
+
def validate_response_status(data)
|
|
182
|
+
return if data.dig('meta', 'status') == true
|
|
183
|
+
raise "Server returned unsuccessful status: #{data.to_json}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if there are more pages to fetch
|
|
187
|
+
def has_more_pages?(data)
|
|
188
|
+
data['meta'] && data['meta']['next'] && data['data'] && !data['data'].empty?
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# vim: sw=2
|
|
196
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
module Learnosity
|
|
4
|
+
module Sdk
|
|
5
|
+
module Utils
|
|
6
|
+
# UUID utility for generating UUIDv4 identifiers
|
|
7
|
+
# Commonly used for user_id and session_id in Learnosity API requests
|
|
8
|
+
class Uuid
|
|
9
|
+
# Generate a UUIDv4 string
|
|
10
|
+
#
|
|
11
|
+
# @return [String] A UUIDv4 string
|
|
12
|
+
def self.generate
|
|
13
|
+
SecureRandom.uuid
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# vim: sw=2
|
|
21
|
+
|
data/lib/learnosity/sdk.rb
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
require "learnosity/sdk/version"
|
|
2
|
+
require "learnosity/sdk/request/data_api"
|
|
3
|
+
require "learnosity/sdk/utils/uuid"
|
|
2
4
|
|
|
3
5
|
module Learnosity
|
|
4
6
|
module Sdk
|
|
5
|
-
#
|
|
7
|
+
# Export DataApi class for convenient access
|
|
8
|
+
DataApi = Request::DataApi
|
|
9
|
+
|
|
10
|
+
# Export Uuid utility for convenient access
|
|
11
|
+
Uuid = Utils::Uuid
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: learnosity-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Olivier Mehani
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2026-01-29 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: sys-uname
|
|
@@ -116,6 +116,7 @@ files:
|
|
|
116
116
|
- docs/quickstart/lrn-sdk-rails/app/controllers/author_controller.rb
|
|
117
117
|
- docs/quickstart/lrn-sdk-rails/app/controllers/authoraide_controller.rb
|
|
118
118
|
- docs/quickstart/lrn-sdk-rails/app/controllers/concerns/.keep
|
|
119
|
+
- docs/quickstart/lrn-sdk-rails/app/controllers/data_api_controller.rb
|
|
119
120
|
- docs/quickstart/lrn-sdk-rails/app/controllers/index_controller.rb
|
|
120
121
|
- docs/quickstart/lrn-sdk-rails/app/controllers/items_controller.rb
|
|
121
122
|
- docs/quickstart/lrn-sdk-rails/app/controllers/questions_controller.rb
|
|
@@ -128,6 +129,7 @@ files:
|
|
|
128
129
|
- docs/quickstart/lrn-sdk-rails/app/models/concerns/.keep
|
|
129
130
|
- docs/quickstart/lrn-sdk-rails/app/views/author/index.html.erb
|
|
130
131
|
- docs/quickstart/lrn-sdk-rails/app/views/authoraide/index.html.erb
|
|
132
|
+
- docs/quickstart/lrn-sdk-rails/app/views/data_api/index.html.erb
|
|
131
133
|
- docs/quickstart/lrn-sdk-rails/app/views/index/index.html.erb
|
|
132
134
|
- docs/quickstart/lrn-sdk-rails/app/views/items/index.html.erb
|
|
133
135
|
- docs/quickstart/lrn-sdk-rails/app/views/layouts/application.html.erb
|
|
@@ -188,14 +190,18 @@ files:
|
|
|
188
190
|
- docs/quickstart/lrn-sdk-rails/tmp/.keep
|
|
189
191
|
- docs/quickstart/lrn-sdk-rails/vendor/assets/javascripts/.keep
|
|
190
192
|
- docs/quickstart/lrn-sdk-rails/vendor/assets/stylesheets/.keep
|
|
193
|
+
- examples/simple/data_api_example.rb
|
|
191
194
|
- examples/simple/init_data.rb
|
|
192
195
|
- examples/simple/init_items.rb
|
|
196
|
+
- examples/simple/uuid_example.rb
|
|
193
197
|
- learnosity-sdk.gemspec
|
|
194
198
|
- lib/learnosity/sdk.rb
|
|
195
199
|
- lib/learnosity/sdk/exceptions.rb
|
|
196
200
|
- lib/learnosity/sdk/request.rb
|
|
201
|
+
- lib/learnosity/sdk/request/data_api.rb
|
|
197
202
|
- lib/learnosity/sdk/request/init.rb
|
|
198
203
|
- lib/learnosity/sdk/utils.rb
|
|
204
|
+
- lib/learnosity/sdk/utils/uuid.rb
|
|
199
205
|
- lib/learnosity/sdk/version.rb
|
|
200
206
|
homepage: https://github.com/Learnosity/learnosity-sdk-ruby/
|
|
201
207
|
licenses:
|
|
@@ -217,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
217
223
|
- !ruby/object:Gem::Version
|
|
218
224
|
version: '0'
|
|
219
225
|
requirements: []
|
|
220
|
-
rubygems_version: 3.
|
|
226
|
+
rubygems_version: 3.4.10
|
|
221
227
|
signing_key:
|
|
222
228
|
specification_version: 4
|
|
223
229
|
summary: SDK to interact with Learnosity APIs
|