rails-xapi 0.1.2
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +152 -0
- data/Rakefile +8 -0
- data/app/controllers/rails_xapi/application_controller.rb +7 -0
- data/app/helpers/rails_xapi/application_helper.rb +26 -0
- data/app/jobs/rails_xapi/application_job.rb +4 -0
- data/app/jobs/rails_xapi/create_statement_job.rb +11 -0
- data/app/mailers/rails_xapi/application_mailer.rb +6 -0
- data/app/models/concerns/serializable.rb +14 -0
- data/app/models/rails_xapi/account.rb +27 -0
- data/app/models/rails_xapi/activity_definition.rb +143 -0
- data/app/models/rails_xapi/actor.rb +227 -0
- data/app/models/rails_xapi/application_record.rb +5 -0
- data/app/models/rails_xapi/context.rb +163 -0
- data/app/models/rails_xapi/context_activity.rb +37 -0
- data/app/models/rails_xapi/errors/xapi_error.rb +4 -0
- data/app/models/rails_xapi/extension.rb +29 -0
- data/app/models/rails_xapi/group_member.rb +21 -0
- data/app/models/rails_xapi/interaction_activity.rb +103 -0
- data/app/models/rails_xapi/interaction_component.rb +34 -0
- data/app/models/rails_xapi/object.rb +150 -0
- data/app/models/rails_xapi/result.rb +174 -0
- data/app/models/rails_xapi/statement.rb +62 -0
- data/app/models/rails_xapi/validators/language_map_validator.rb +64 -0
- data/app/models/rails_xapi/verb.rb +260 -0
- data/app/services/application_service.rb +16 -0
- data/app/services/rails_xapi/query.rb +217 -0
- data/app/services/rails_xapi/statement_creator.rb +53 -0
- data/app/views/layouts/rails_xapi/application.html.erb +12 -0
- data/config/locales/rails_xapi.en.yml +43 -0
- data/config/locales/rails_xapi.fr.yml +233 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20240716144226_create_rails_xapi_actors.rb +16 -0
- data/db/migrate/20240716144227_create_rails_xapi_accounts.rb +15 -0
- data/db/migrate/20240716144228_create_rails_xapi_verbs.rb +14 -0
- data/db/migrate/20240716144229_create_rails_xapi_objects.rb +16 -0
- data/db/migrate/20240716144230_create_rails_xapi_activity_definitions.rb +17 -0
- data/db/migrate/20240716144231_create_rails_xapi_extensions.rb +13 -0
- data/db/migrate/20240716144232_create_rails_xapi_statements.rb +19 -0
- data/db/migrate/20240716144233_create_rails_xapi_results.rb +21 -0
- data/db/migrate/20240716144234_create_rails_xapi_contexts.rb +23 -0
- data/db/migrate/20240716144235_create_rails_xapi_context_activities.rb +16 -0
- data/db/migrate/20240716144236_create_rails_xapi_group_members.rb +16 -0
- data/db/migrate/20250522093846_create_rails_xapi_interaction_activities.rb +15 -0
- data/db/migrate/20250522122830_create_rails_xapi_interaction_component.rb +16 -0
- data/lib/rails-xapi/configuration.rb +13 -0
- data/lib/rails-xapi/engine.rb +28 -0
- data/lib/rails-xapi/version.rb +5 -0
- data/lib/rails-xapi.rb +16 -0
- data/lib/tasks/auto_annotate_models.rake +62 -0
- data/lib/tasks/rails-xapi_tasks.rake +4 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f7f793c9d2d10b489f20f41d63f0687b2d38ad974792cd12600a98846634178
|
4
|
+
data.tar.gz: cd86c11505ed01f92f714949a1c34fd1ed12de12cc170e62409189b4885b3a77
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1b17754744f9266c1163bc1a38a1b48559b4e349de9d586216de28a097b904a4983f47c447895e4fe20040160d8779a21490dad14fe7eddde91a4b278c2d9668
|
7
|
+
data.tar.gz: d32ac593f6b50267cbfc491cd2e0a88cd79cb616f7d017f4822149ce50f851d0c74c9cf0c8a79ff4fc137c49f5741fee0e9a83c6e97d720909cf38fadec649a0
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Hipjea
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Rails xAPI
|
2
|
+
|
3
|
+
This gem is a Rails engine that allows the validation of data from an xAPI statement. It enables the storage of xAPI statements in relational tables.
|
4
|
+
|
5
|
+
[](https://github.com/fondation-unit/rails-xapi/actions/workflows/ci.yml/)
|
6
|
+
|
7
|
+
> [!IMPORTANT]
|
8
|
+
> This is an ongoing development. The documentation will be provided as it becomes available.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem "rails-xapi", git: "https://github.com/fondation-unit/rails-xapi"
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
$ bundle
|
22
|
+
```
|
23
|
+
|
24
|
+
Create the migration files:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
$ bin/rails rails_xapi:install:migrations
|
28
|
+
```
|
29
|
+
|
30
|
+
Mount the engine in `config/routes.rb`:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
mount RailsXapi::Engine, at: "rails-xapi"
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Statement creation
|
39
|
+
|
40
|
+
Example usage of the `RailsXapi::StatementCreator` service:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
user_name = "#{user.firstname} #{user.lastname}"
|
44
|
+
|
45
|
+
data = {
|
46
|
+
actor: {
|
47
|
+
objectType: "Agent",
|
48
|
+
name: user_name,
|
49
|
+
mbox: "mailto:#{user.email}",
|
50
|
+
account: {
|
51
|
+
homePage: "http://example.com/some_user_homepage/#{user&.id}",
|
52
|
+
name: user_name
|
53
|
+
}
|
54
|
+
},
|
55
|
+
verb: {
|
56
|
+
id: "https://brindlewaye.com/xAPITerms/verbs/loggedin/"
|
57
|
+
},
|
58
|
+
object: {
|
59
|
+
id: new_user_session_url,
|
60
|
+
definition: {
|
61
|
+
name: {
|
62
|
+
"en-US" => "log in"
|
63
|
+
},
|
64
|
+
description: {
|
65
|
+
"en-US" => "User signed in"
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
RailsXapi::StatementCreator.create(data)
|
72
|
+
```
|
73
|
+
|
74
|
+
### Data query
|
75
|
+
|
76
|
+
Ready-to-use queries are available in the [app/services/rails_xapi/query.rb](app/services/rails_xapi/query.rb) class.
|
77
|
+
|
78
|
+
| Query symbol | Description |
|
79
|
+
| ---------------------------------- | --------------------------------------------------------- |
|
80
|
+
| `:statement` | Retrieve a single statement by its ID |
|
81
|
+
| `:statements_by_object_and_actors` | Get statements by object IDand actor emails |
|
82
|
+
| `:verb_ids` | Get list of unique verb IDs |
|
83
|
+
| `:verb_displays` | Get list of unique verb display values |
|
84
|
+
| `:verbs` | Get hash of unique verbs with ID and display values |
|
85
|
+
| `:actor_by_email` | Find statements by actor's email |
|
86
|
+
| `:actor_by_mbox` | Find statements by actor's mbox |
|
87
|
+
| `:actor_by_account_homepage` | Find statements by actor's account homepage |
|
88
|
+
| `:actor_by_openid` | Find statements by actor's openID |
|
89
|
+
| `:actor_by_mbox_sha1sum` | Find statements by actor's mbox SHA1 sum |
|
90
|
+
| `:user_statements_per_month` | Retrieve actor's statements for a specific month/year |
|
91
|
+
| `:per_month` | Group given records by creation date for a specific month |
|
92
|
+
| `:month_graph_data` | Create date/count array of data for a month |
|
93
|
+
|
94
|
+
Example of usage:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
def create_statement
|
98
|
+
data = {
|
99
|
+
actor: {
|
100
|
+
objectType: "Agent",
|
101
|
+
name: "John Doe",
|
102
|
+
mbox: "mailto:example@localhost.com",
|
103
|
+
account: {
|
104
|
+
homePage: "http://example.com/some_user_homepage/1",
|
105
|
+
name: "JohnDoe#1"
|
106
|
+
}
|
107
|
+
},
|
108
|
+
verb: {
|
109
|
+
id: "https://brindlewaye.com/xAPITerms/verbs/loggedin/"
|
110
|
+
},
|
111
|
+
object: {
|
112
|
+
id: "http://localhost:3000/new_user_session",
|
113
|
+
definition: {
|
114
|
+
name: {
|
115
|
+
"en-GB" => "login"
|
116
|
+
},
|
117
|
+
description: {
|
118
|
+
"en-US" => "User signed in."
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
statement = RailsXapi::StatementCreator.create(data)
|
125
|
+
redirect_to statement_show_path(id: statement[:statement][:id])
|
126
|
+
end
|
127
|
+
|
128
|
+
def statement_show
|
129
|
+
@statement = RailsXapi::Query.call(
|
130
|
+
query: :statement,
|
131
|
+
args: params[:id]
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def logs_per_month(year = Date.current.year, month = Date.current.month)
|
136
|
+
RailsXapi::Query.call(
|
137
|
+
query: :user_statements_per_month,
|
138
|
+
args: [{mbox: "mailto:#{email}"}, year, month]
|
139
|
+
)
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
## Test
|
144
|
+
|
145
|
+
```bash
|
146
|
+
bundle exec rails db:schema:load RAILS_ENV=test
|
147
|
+
bundle exec rspec spec/
|
148
|
+
```
|
149
|
+
|
150
|
+
## License
|
151
|
+
|
152
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsXapi
|
4
|
+
module ApplicationHelper
|
5
|
+
# Output the duration ISO 8601 in minutes.
|
6
|
+
def duration_to_minutes(duration)
|
7
|
+
sprintf("%.2f", ActiveSupport::Duration.parse(duration)&.in_minutes)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Output the value of a JSON row in a specific locale.
|
11
|
+
def json_value_for_locale(json_str, locale = I18n.locale)
|
12
|
+
hash = JSON.parse(json_str)
|
13
|
+
result = hash.select { |key, _value| key.include?(locale.to_s) }
|
14
|
+
result.values.first.to_s || hash.first.value.to_s
|
15
|
+
rescue
|
16
|
+
json_str
|
17
|
+
end
|
18
|
+
|
19
|
+
# Output the result score as a percentage.
|
20
|
+
def result_success_rate(result)
|
21
|
+
return nil if result.score_raw.blank? || result.score_max.blank?
|
22
|
+
|
23
|
+
((result.score_raw.to_f / result.score_max.to_f) * 100).to_i
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Serializable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
LATIN_LETTERS = "a-zA-ZÀ-ÖØ-öø-ÿœ"
|
7
|
+
LATIN_LETTERS_REGEX = /[^#{LATIN_LETTERS}\s-]/i
|
8
|
+
|
9
|
+
included do
|
10
|
+
def serialized_value(data)
|
11
|
+
data.is_a?(Hash) ? data.to_json : data.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Represents an account with home_page and name.
|
4
|
+
class RailsXapi::Account < ApplicationRecord
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
belongs_to :actor, class_name: "RailsXapi::Actor", dependent: :destroy
|
8
|
+
|
9
|
+
def homePage=(value)
|
10
|
+
# We need to match the camel case notation from JSON data.
|
11
|
+
self.home_page = value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# == Schema Information
|
16
|
+
#
|
17
|
+
# Table name: rails_xapi_accounts
|
18
|
+
#
|
19
|
+
# id :integer not null, primary key
|
20
|
+
# home_page :string not null
|
21
|
+
# name :string not null
|
22
|
+
# actor_id :bigint not null
|
23
|
+
#
|
24
|
+
# Indexes
|
25
|
+
#
|
26
|
+
# index_rails_xapi_accounts_on_actor_id (actor_id)
|
27
|
+
#
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The object optional activity definition.
|
4
|
+
# See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#activity-definition
|
5
|
+
class RailsXapi::ActivityDefinition < ApplicationRecord
|
6
|
+
include Serializable
|
7
|
+
include RailsXapi::ApplicationHelper
|
8
|
+
|
9
|
+
belongs_to :object, class_name: "RailsXapi::Object"
|
10
|
+
has_one :interaction_activity,
|
11
|
+
class_name: "RailsXapi::InteractionActivity",
|
12
|
+
dependent: :destroy
|
13
|
+
has_many :extensions, as: :extendable, dependent: :destroy
|
14
|
+
|
15
|
+
validates :activity_type,
|
16
|
+
format: {
|
17
|
+
with: %r{\A\w+://\S+\z},
|
18
|
+
message: I18n.t("rails_xapi.errors.must_be_a_valid_iri")
|
19
|
+
},
|
20
|
+
allow_blank: true
|
21
|
+
|
22
|
+
before_validation :set_name, :set_description
|
23
|
+
validates_with RailsXapi::Validators::LanguageMapValidator,
|
24
|
+
attributes: %i[name description]
|
25
|
+
|
26
|
+
def type
|
27
|
+
# Virtual attribute to bypass the Single Table Inheritance keyword.
|
28
|
+
activity_type
|
29
|
+
end
|
30
|
+
|
31
|
+
def type=(value)
|
32
|
+
# Store the `type` attribute into `activity_type` column to avoid
|
33
|
+
# reserved key-words issues.
|
34
|
+
self.activity_type = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def moreInfo=(value)
|
38
|
+
# Match the camel case notation from JSON data.
|
39
|
+
self.more_info = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def assign_from_json_definition(definition_hash)
|
43
|
+
return unless definition_hash.present?
|
44
|
+
|
45
|
+
normalized_hash = definition_hash.deep_stringify_keys
|
46
|
+
interaction_keys = RailsXapi::InteractionActivity::INTERACTION_KEYS
|
47
|
+
|
48
|
+
interaction_attrs = normalized_hash.slice(*interaction_keys)
|
49
|
+
core_attrs = normalized_hash.except(*interaction_keys)
|
50
|
+
# Assign base definition attributes
|
51
|
+
self.attributes = core_attrs
|
52
|
+
|
53
|
+
if normalized_hash["interactionType"].present?
|
54
|
+
# Assign InteractionActivity attributes
|
55
|
+
build_interaction_activity unless interaction_activity
|
56
|
+
|
57
|
+
interaction_activity.interaction_type =
|
58
|
+
interaction_attrs["interactionType"]
|
59
|
+
interaction_activity.correct_responses_pattern =
|
60
|
+
interaction_attrs["correctResponsesPattern"]
|
61
|
+
# Build the interaction_components association
|
62
|
+
interaction_activity.assign_interaction_components(interaction_attrs)
|
63
|
+
|
64
|
+
# Trigger the validation
|
65
|
+
unless interaction_activity.valid?
|
66
|
+
raise ActiveRecord::RecordInvalid, interaction_activity
|
67
|
+
end
|
68
|
+
|
69
|
+
interaction_activity.save!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def extensions=(extensions_data)
|
74
|
+
unless extensions_data.is_a?(Hash)
|
75
|
+
raise RailsXapi::Errors::XapiError,
|
76
|
+
I18n.t(
|
77
|
+
"rails_xapi.errors.attribute_must_be_a_hash",
|
78
|
+
name: "extensions"
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Find any existing extension for the given activity definition.
|
83
|
+
exts = extensions.where(extendable_type: self.class.to_s, extendable_id: id)
|
84
|
+
|
85
|
+
# If none, build and save the extensions.
|
86
|
+
if exts.blank?
|
87
|
+
extensions_data.each do |iri, data|
|
88
|
+
extensions.build(iri: iri, value: serialized_value(data))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def as_json
|
94
|
+
{ name: name, description: description, type: activity_type }.tap do |hash|
|
95
|
+
hash[:extensions] = extensions.as_json if extensions.present?
|
96
|
+
hash[:moreInfo] = more_info if more_info.present?
|
97
|
+
hash.merge!(interaction_activity.as_json) if interaction_activity.present?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def set_json_attribute(attribute)
|
104
|
+
value = send(attribute)
|
105
|
+
return if value.blank?
|
106
|
+
|
107
|
+
begin
|
108
|
+
value = JSON.parse(value.to_s.gsub("=>", ":"))
|
109
|
+
rescue JSON::ParserError => _
|
110
|
+
raise RailsXapi::Errors::XapiError,
|
111
|
+
I18n.t(
|
112
|
+
"rails_xapi.errors.attribute_must_be_a_valid_language_map",
|
113
|
+
name: attribute
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
self[attribute] = value.to_json
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_name
|
121
|
+
set_json_attribute(:name)
|
122
|
+
end
|
123
|
+
|
124
|
+
def set_description
|
125
|
+
set_json_attribute(:description)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# == Schema Information
|
130
|
+
#
|
131
|
+
# Table name: rails_xapi_activity_definitions
|
132
|
+
#
|
133
|
+
# id :integer not null, primary key
|
134
|
+
# activity_type :string
|
135
|
+
# description :text
|
136
|
+
# more_info :text
|
137
|
+
# name :string
|
138
|
+
# object_id :string not null
|
139
|
+
#
|
140
|
+
# Indexes
|
141
|
+
#
|
142
|
+
# index_rails_xapi_activity_definitions_on_object_id (object_id)
|
143
|
+
#
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The Actor defines who performed the action.
|
4
|
+
# See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#242-actor
|
5
|
+
class RailsXapi::Actor < ApplicationRecord
|
6
|
+
require "uri"
|
7
|
+
include Serializable
|
8
|
+
|
9
|
+
OBJECT_TYPES = %w[Agent Group]
|
10
|
+
|
11
|
+
attr_accessor :objectType, :member
|
12
|
+
|
13
|
+
has_one :account, class_name: "RailsXapi::Account", dependent: :destroy
|
14
|
+
has_many :statements, class_name: "RailsXapi::Statement", dependent: :nullify
|
15
|
+
has_many :members, class_name: "RailsXapi::GroupMember", dependent: :destroy
|
16
|
+
|
17
|
+
validates :object_type, presence: true
|
18
|
+
validate :validate_actor_ifi_presence,
|
19
|
+
:validate_mbox,
|
20
|
+
:validate_mbox_sha1sum,
|
21
|
+
:validate_object_type,
|
22
|
+
:validate_openid
|
23
|
+
|
24
|
+
after_initialize :set_defaults
|
25
|
+
before_validation :normalize_actor
|
26
|
+
after_commit :create_members, if: :is_group?
|
27
|
+
|
28
|
+
# Build the Actor object from the given data and user email.
|
29
|
+
#
|
30
|
+
# @param [Hash] data The data used to build the actor object, including optional nested account data.
|
31
|
+
# @return [RailsXapi::Actor] The actor object initialized with the data.
|
32
|
+
def self.build_actor_from_data(data)
|
33
|
+
data = handle_account_data(data)
|
34
|
+
|
35
|
+
conditions = data.slice(:mbox, :mbox_sha1sum, :openid).compact
|
36
|
+
find_by(conditions) || create(data)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Find an Actor by its identifiers or create a new one.
|
40
|
+
#
|
41
|
+
# @param [Hash] data The data to find or create the actor.
|
42
|
+
# @return [RailsXapi::Actor] The found or created actor object.
|
43
|
+
def self.by_iri_or_create(data)
|
44
|
+
data = handle_account_data(data)
|
45
|
+
|
46
|
+
actor =
|
47
|
+
find_or_create_by(
|
48
|
+
mbox: data[:mbox],
|
49
|
+
mbox_sha1sum: data[:mbox_sha1sum],
|
50
|
+
openid: data[:openid]
|
51
|
+
) { |a| a.attributes = data }
|
52
|
+
|
53
|
+
unless actor.valid?
|
54
|
+
raise RailsXapi::Errors::XapiError,
|
55
|
+
I18n.t("rails_xapi.errors.invalid_actor")
|
56
|
+
end
|
57
|
+
|
58
|
+
actor
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_mbox
|
62
|
+
return if mbox.blank?
|
63
|
+
|
64
|
+
mbox_valid =
|
65
|
+
mbox.strip =~ /\Amailto:([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
|
66
|
+
unless mbox_valid
|
67
|
+
raise RailsXapi::Errors::XapiError,
|
68
|
+
I18n.t("rails_xapi.errors.malformed_mbox", name: mbox)
|
69
|
+
end
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
# Overrides the Hash class method to camelize object_type, according to the xAPI specification.
|
75
|
+
# See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#part-two-experience-api-data
|
76
|
+
#
|
77
|
+
# @return [Hash] The actor hash with the camel-case version of object_type.
|
78
|
+
def as_json
|
79
|
+
{
|
80
|
+
objectType: object_type,
|
81
|
+
name: name,
|
82
|
+
mbox: mbox,
|
83
|
+
mbox_sha1sum: mbox_sha1sum,
|
84
|
+
account: account.as_json,
|
85
|
+
openid: openid
|
86
|
+
}.compact
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def set_defaults
|
92
|
+
# We need to match the camel case notation from JSON data.
|
93
|
+
self.object_type =
|
94
|
+
objectType.presence || object_type.presence || OBJECT_TYPES.first
|
95
|
+
self.member = member.presence || nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def is_group?
|
99
|
+
object_type === "Group"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Normalizes the actor data.
|
103
|
+
#
|
104
|
+
# @param [Hash] actor The actor data.
|
105
|
+
# @return [Hash] The normalized actor data.
|
106
|
+
def normalize_actor
|
107
|
+
self.object_type = object_type.presence || OBJECT_TYPES.first
|
108
|
+
|
109
|
+
if name.present?
|
110
|
+
self.name =
|
111
|
+
name
|
112
|
+
.gsub(Serializable::LATIN_LETTERS_REGEX, "")
|
113
|
+
.to_s
|
114
|
+
.humanize
|
115
|
+
.gsub(/\b('?[#{Serializable::LATIN_LETTERS}])/o) do
|
116
|
+
Regexp.last_match(1).capitalize
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
self.mbox = mbox.strip.downcase if mbox.present?
|
121
|
+
end
|
122
|
+
|
123
|
+
# Find an Account by its identifier or create a new one and set the actor's data.
|
124
|
+
#
|
125
|
+
# @param [Hash] data The data to find or create the account.
|
126
|
+
# @return [Hash] The actor's data.
|
127
|
+
private_class_method def self.handle_account_data(data)
|
128
|
+
if (account_data = data[:account]).present?
|
129
|
+
account =
|
130
|
+
RailsXapi::Account.find_or_create_by(
|
131
|
+
home_page: account_data[:homePage]
|
132
|
+
) { |a| a.name = account_data[:name] }
|
133
|
+
|
134
|
+
data[:account] = account
|
135
|
+
data[:name] ||= account_data[:name]
|
136
|
+
end
|
137
|
+
|
138
|
+
data
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_actor_ifi_presence
|
142
|
+
unless mbox.present? || mbox_sha1sum.present? || openid.present? ||
|
143
|
+
account.present?
|
144
|
+
raise RailsXapi::Errors::XapiError,
|
145
|
+
I18n.t("rails_xapi.errors.actor_ifi_must_be_present")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_mbox_sha1sum
|
150
|
+
return if mbox_sha1sum.blank?
|
151
|
+
|
152
|
+
unless is_sha1?(mbox_sha1sum)
|
153
|
+
raise RailsXapi::Errors::XapiError,
|
154
|
+
I18n.t("rails_xapi.errors.malformed_mbox_sha1sum")
|
155
|
+
end
|
156
|
+
|
157
|
+
true
|
158
|
+
end
|
159
|
+
|
160
|
+
def validate_object_type
|
161
|
+
object_type_valid = OBJECT_TYPES.include?(object_type)
|
162
|
+
unless object_type_valid
|
163
|
+
raise RailsXapi::Errors::XapiError,
|
164
|
+
I18n.t(
|
165
|
+
"rails_xapi.errors.invalid_actor_object_type",
|
166
|
+
name: object_type
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
true
|
171
|
+
end
|
172
|
+
|
173
|
+
def validate_openid
|
174
|
+
return if openid.blank?
|
175
|
+
|
176
|
+
uri = URI.parse(openid)
|
177
|
+
is_valid_openid_uri = uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
178
|
+
unless is_valid_openid_uri
|
179
|
+
raise RailsXapi::Errors::XapiError,
|
180
|
+
I18n.t("rails_xapi.errors.malformed_openid_uri", uri: openid)
|
181
|
+
end
|
182
|
+
|
183
|
+
true
|
184
|
+
end
|
185
|
+
|
186
|
+
# Produces the hex-encoded SHA1 hash of the actor mailto.
|
187
|
+
#
|
188
|
+
# @param [String] mbox The mbox clear value to be encoded.
|
189
|
+
# @return [Boolean] True if the value is matching, false otherwise.
|
190
|
+
def is_sha1?(str)
|
191
|
+
# SHA-1 hash is a 40-character hexadecimal string consisting of numbers 0-9 and letters a-f.
|
192
|
+
# We also handle the case with an optional "sha1" prefix.
|
193
|
+
!!(str =~ /^(sha1:)?[0-9a-f]{40}$/i)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Create members in the case of a "Group" objectType.
|
197
|
+
def create_members
|
198
|
+
# We should end the function here when we create a group without members (ex: a team in the context object).
|
199
|
+
return if member.blank?
|
200
|
+
|
201
|
+
if id.blank?
|
202
|
+
raise RailsXapi::Errors::XapiError,
|
203
|
+
I18n.t("rails_xapi.errors.failed_to_create_group_members")
|
204
|
+
end
|
205
|
+
|
206
|
+
member.each do |m|
|
207
|
+
new_actor = RailsXapi::Actor.by_iri_or_create(m)
|
208
|
+
RailsXapi::GroupMember.create(group_id: id, actor_id: new_actor.id)
|
209
|
+
rescue => _
|
210
|
+
raise RailsXapi::Errors::XapiError,
|
211
|
+
I18n.t("rails_xapi.errors.failed_to_create_member", member: m)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# == Schema Information
|
217
|
+
#
|
218
|
+
# Table name: rails_xapi_actors
|
219
|
+
#
|
220
|
+
# id :integer not null, primary key
|
221
|
+
# mbox :string
|
222
|
+
# mbox_sha1sum :string
|
223
|
+
# name :string
|
224
|
+
# object_type :string
|
225
|
+
# openid :string
|
226
|
+
# created_at :datetime not null
|
227
|
+
#
|